Add business Finance hub with invoices and templates.
Ship invoice issue/list/templates under Finance, fix tenant theme reset and owner role badge, and show customers-joined charts by Persian months. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
722933dda9
commit
917b840ee5
@@ -0,0 +1,542 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FileStack, FileText, Pencil, Plus, Settings as SettingsIcon, Trash2 } from 'lucide-react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||
import { Modal } from '../components/Modal'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { isEmptyRichText, richTextToPlain } from '../components/RichTextEditor'
|
||||
import { Tooltip } from '../components/Tooltip'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError, isAbortError } from '../lib/api'
|
||||
import {
|
||||
createInvoiceItemTemplate,
|
||||
deleteInvoiceItemTemplate,
|
||||
deleteInvoiceTemplate,
|
||||
listInvoiceItemTemplates,
|
||||
listInvoiceTemplates,
|
||||
updateInvoiceItemTemplate,
|
||||
} from '../services/invoiceService'
|
||||
import type { InvoiceItemTemplate, InvoiceTemplate } from '../types/invoice'
|
||||
import { formatIrtInput, formatIrtPrice, parseIrtInput } from '../utils/irtPrice'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import formStyles from '../components/InvoiceForm.module.css'
|
||||
import tableStyles from './InvoicesPage.module.css'
|
||||
import styles from './InvoiceTemplatesPage.module.css'
|
||||
|
||||
type ItemDraft = {
|
||||
title: string
|
||||
duration: string
|
||||
worktime: string
|
||||
description: string
|
||||
price: string
|
||||
discountedPrice: string
|
||||
}
|
||||
|
||||
const EMPTY_ITEM_DRAFT: ItemDraft = {
|
||||
title: '',
|
||||
duration: '',
|
||||
worktime: '',
|
||||
description: '',
|
||||
price: '',
|
||||
discountedPrice: '',
|
||||
}
|
||||
|
||||
function itemDraftFromTemplate(template: InvoiceItemTemplate): ItemDraft {
|
||||
return {
|
||||
title: template.title,
|
||||
duration: template.duration ?? '',
|
||||
worktime: template.worktime ?? '',
|
||||
description: template.description ?? '',
|
||||
price: formatIrtInput(String(Math.round(template.price))),
|
||||
discountedPrice:
|
||||
template.discountedPrice === null || template.discountedPrice === undefined
|
||||
? ''
|
||||
: formatIrtInput(String(Math.round(template.discountedPrice))),
|
||||
}
|
||||
}
|
||||
|
||||
export function InvoiceTemplatesPage() {
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
const t = useT()
|
||||
|
||||
const [itemTemplates, setItemTemplates] = useState<InvoiceItemTemplate[]>([])
|
||||
const [invoiceTemplates, setInvoiceTemplates] = useState<InvoiceTemplate[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [itemEditorOpen, setItemEditorOpen] = useState(false)
|
||||
const [editingItem, setEditingItem] = useState<InvoiceItemTemplate | null>(null)
|
||||
const [itemDraft, setItemDraft] = useState<ItemDraft>(EMPTY_ITEM_DRAFT)
|
||||
const [itemSubmitting, setItemSubmitting] = useState(false)
|
||||
const [itemFormError, setItemFormError] = useState('')
|
||||
const [removeItemTarget, setRemoveItemTarget] = useState<InvoiceItemTemplate | null>(null)
|
||||
const [removeTplTarget, setRemoveTplTarget] = useState<InvoiceTemplate | null>(null)
|
||||
|
||||
function itemDraftToPayload(draft: ItemDraft) {
|
||||
const price = parseIrtInput(draft.price)
|
||||
if (!draft.title.trim()) throw new Error(t('invoiceDraft.error.itemTitle'))
|
||||
if (price === null) throw new Error(t('invoiceDraft.error.itemPrice', { title: draft.title.trim() }))
|
||||
const discountedPrice = draft.discountedPrice.trim() ? parseIrtInput(draft.discountedPrice) : null
|
||||
if (draft.discountedPrice.trim() && discountedPrice === null) {
|
||||
throw new Error(t('invoiceDraft.error.itemDiscountInvalid', { title: draft.title.trim() }))
|
||||
}
|
||||
if (discountedPrice !== null && discountedPrice > price) {
|
||||
throw new Error(t('invoiceDraft.error.itemDiscountExceeds', { title: draft.title.trim() }))
|
||||
}
|
||||
return {
|
||||
title: draft.title.trim(),
|
||||
duration: draft.duration.trim() || undefined,
|
||||
worktime: draft.worktime.trim() || undefined,
|
||||
description: draft.description.trim() || undefined,
|
||||
price,
|
||||
discountedPrice,
|
||||
}
|
||||
}
|
||||
|
||||
async function reload(signal?: AbortSignal) {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const [items, templates] = await Promise.all([
|
||||
listInvoiceItemTemplates(signal),
|
||||
listInvoiceTemplates(signal),
|
||||
])
|
||||
setItemTemplates(items.items)
|
||||
setInvoiceTemplates(templates.items)
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
setError(err instanceof ApiError ? err.message : t('invoiceTemplates.loadError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
void reload(controller.signal)
|
||||
return () => controller.abort()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
function openCreateItem() {
|
||||
setEditingItem(null)
|
||||
setItemDraft(EMPTY_ITEM_DRAFT)
|
||||
setItemFormError('')
|
||||
setItemEditorOpen(true)
|
||||
}
|
||||
|
||||
function openEditItem(template: InvoiceItemTemplate) {
|
||||
setEditingItem(template)
|
||||
setItemDraft(itemDraftFromTemplate(template))
|
||||
setItemFormError('')
|
||||
setItemEditorOpen(true)
|
||||
}
|
||||
|
||||
async function handleSaveItem() {
|
||||
setItemFormError('')
|
||||
let payload
|
||||
try {
|
||||
payload = itemDraftToPayload(itemDraft)
|
||||
} catch (err) {
|
||||
setItemFormError(err instanceof Error ? err.message : t('invoiceTemplates.itemSaveError'))
|
||||
return
|
||||
}
|
||||
setItemSubmitting(true)
|
||||
try {
|
||||
if (editingItem) {
|
||||
await updateInvoiceItemTemplate(editingItem.id, payload)
|
||||
showToast(t('invoiceTemplates.itemUpdated'), 'success')
|
||||
} else {
|
||||
await createInvoiceItemTemplate(payload)
|
||||
showToast(t('invoiceTemplates.itemCreated'), 'success')
|
||||
}
|
||||
setItemEditorOpen(false)
|
||||
await reload()
|
||||
} catch (err) {
|
||||
setItemFormError(err instanceof ApiError ? err.message : t('invoiceTemplates.itemSaveError'))
|
||||
} finally {
|
||||
setItemSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveItem() {
|
||||
if (!removeItemTarget) return
|
||||
try {
|
||||
await deleteInvoiceItemTemplate(removeItemTarget.id)
|
||||
showToast(t('invoiceTemplates.itemRemoved'), 'success')
|
||||
setRemoveItemTarget(null)
|
||||
await reload()
|
||||
} catch (err) {
|
||||
showToast(err instanceof ApiError ? err.message : t('invoiceTemplates.itemRemoveError'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveTpl() {
|
||||
if (!removeTplTarget) return
|
||||
try {
|
||||
await deleteInvoiceTemplate(removeTplTarget.id)
|
||||
showToast(t('invoiceTemplates.templateRemoved'), 'success')
|
||||
setRemoveTplTarget(null)
|
||||
await reload()
|
||||
} catch (err) {
|
||||
showToast(err instanceof ApiError ? err.message : t('invoiceTemplates.templateRemoveError'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: t('nav.home'), href: '/' },
|
||||
{ label: t('nav.finance'), href: '/finance' },
|
||||
{ label: t('title.invoices'), href: '/invoices' },
|
||||
{ label: t('title.invoiceTemplates') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div>
|
||||
<PageTitle en="INVOICE TEMPLATES">{t('title.invoiceTemplates')}</PageTitle>
|
||||
<p className={pageStyles.pageSubtitle}>{t('invoiceTemplates.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <p className={formStyles.alertError}>{error}</p> : null}
|
||||
|
||||
<section className={formStyles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionTitleRow}>
|
||||
<FileStack size={18} />
|
||||
<div>
|
||||
<h3 className={styles.sectionTitle}>{t('invoiceTemplates.section.templates.title')}</h3>
|
||||
<p className={styles.sectionSubtitle}>{t('invoiceTemplates.section.templates.desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`${formStyles.btn} ${formStyles.btnPrimary}`}
|
||||
onClick={() => navigate('/invoices/templates/new')}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t('invoiceTemplates.addTemplate')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={tableStyles.tableWrap}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<div className={tableStyles.meta}>
|
||||
{loading
|
||||
? t('invoices.loading')
|
||||
: invoiceTemplates.length === 1
|
||||
? t('invoiceTemplates.countTemplatesOne', { count: invoiceTemplates.length })
|
||||
: t('invoiceTemplates.countTemplatesMany', { count: invoiceTemplates.length })}
|
||||
</div>
|
||||
</div>
|
||||
<table className={`${tableStyles.table} ${styles.invoiceTemplatesTable}`}>
|
||||
<colgroup>
|
||||
<col className={styles.nameCol} />
|
||||
<col className={styles.countCol} />
|
||||
<col className={styles.countCol} />
|
||||
<col className={styles.countCol} />
|
||||
<col className={styles.actionsCol} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.name')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.items')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.keyPoints')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.accounts')}</th>
|
||||
<th className={`${tableStyles.th} ${tableStyles.thActions}`}>
|
||||
{t('invoiceTemplates.col.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!loading && invoiceTemplates.length === 0 ? (
|
||||
<tr>
|
||||
<td className={tableStyles.td} colSpan={5}>
|
||||
<div className={styles.emptyState}>
|
||||
<SettingsIcon size={20} />
|
||||
<span>{t('invoiceTemplates.emptyTemplates')}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{invoiceTemplates.map((template) => (
|
||||
<tr key={template.id}>
|
||||
<td className={`${tableStyles.td} ${styles.nameCell}`}>
|
||||
<div className={tableStyles.itemTitle}>{template.name}</div>
|
||||
{template.topText && !isEmptyRichText(template.topText) ? (
|
||||
<div
|
||||
className={`${tableStyles.subText} ${styles.topTextPreview}`}
|
||||
title={richTextToPlain(template.topText)}
|
||||
>
|
||||
{richTextToPlain(template.topText)}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className={tableStyles.td}>{template.items.length}</td>
|
||||
<td className={tableStyles.td}>{template.keyPoints.length}</td>
|
||||
<td className={tableStyles.td}>{template.accounts.length}</td>
|
||||
<td className={`${tableStyles.td} ${tableStyles.tdActions}`}>
|
||||
<div className={tableStyles.rowActions}>
|
||||
<Tooltip label={t('invoiceTemplates.editTemplate')}>
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.actionBtn}
|
||||
onClick={() => navigate(`/invoices/templates/${template.id}`)}
|
||||
aria-label={t('invoiceTemplates.editTemplate')}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('invoiceTemplates.removeTemplate')}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${tableStyles.actionBtn} ${tableStyles.actionBtnDanger}`}
|
||||
onClick={() => setRemoveTplTarget(template)}
|
||||
aria-label={t('invoiceTemplates.removeTemplate')}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={`${formStyles.section} ${formStyles.sectionSpaced}`}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionTitleRow}>
|
||||
<FileText size={18} />
|
||||
<div>
|
||||
<h3 className={styles.sectionTitle}>{t('invoiceTemplates.section.items.title')}</h3>
|
||||
<p className={styles.sectionSubtitle}>{t('invoiceTemplates.section.items.desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`${formStyles.btn} ${formStyles.btnPrimary}`}
|
||||
onClick={openCreateItem}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t('invoiceTemplates.addItem')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={tableStyles.tableWrap}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<div className={tableStyles.meta}>
|
||||
{loading
|
||||
? t('invoices.loading')
|
||||
: itemTemplates.length === 1
|
||||
? t('invoiceTemplates.countItemsOne', { count: itemTemplates.length })
|
||||
: t('invoiceTemplates.countItemsMany', { count: itemTemplates.length })}
|
||||
</div>
|
||||
</div>
|
||||
<table className={`${tableStyles.table} ${styles.itemTemplatesTable}`}>
|
||||
<colgroup>
|
||||
<col className={styles.nameCol} />
|
||||
<col className={styles.durationCol} />
|
||||
<col className={styles.worktimeCol} />
|
||||
<col className={styles.priceCol} />
|
||||
<col className={styles.discountedCol} />
|
||||
<col className={styles.actionsCol} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.title')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.duration')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.worktime')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.price')}</th>
|
||||
<th className={tableStyles.th}>{t('invoiceTemplates.col.discounted')}</th>
|
||||
<th className={`${tableStyles.th} ${tableStyles.thActions}`}>
|
||||
{t('invoiceTemplates.col.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!loading && itemTemplates.length === 0 ? (
|
||||
<tr>
|
||||
<td className={tableStyles.td} colSpan={6}>
|
||||
<div className={styles.emptyState}>
|
||||
<SettingsIcon size={20} />
|
||||
<span>{t('invoiceTemplates.emptyItems')}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{itemTemplates.map((template) => (
|
||||
<tr key={template.id}>
|
||||
<td className={`${tableStyles.td} ${styles.nameCell}`}>
|
||||
<div className={tableStyles.itemTitle}>{template.title}</div>
|
||||
{template.description ? (
|
||||
<div className={`${tableStyles.subText} ${styles.nameCellDesc}`}>
|
||||
{template.description}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className={tableStyles.td}>{template.duration || '—'}</td>
|
||||
<td className={tableStyles.td}>{template.worktime || '—'}</td>
|
||||
<td className={tableStyles.td}>{formatIrtPrice(template.price)}</td>
|
||||
<td className={tableStyles.td}>
|
||||
{template.discountedPrice === null ? '—' : formatIrtPrice(template.discountedPrice)}
|
||||
</td>
|
||||
<td className={`${tableStyles.td} ${tableStyles.tdActions}`}>
|
||||
<div className={tableStyles.rowActions}>
|
||||
<Tooltip label={t('invoiceTemplates.editItem')}>
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.actionBtn}
|
||||
onClick={() => openEditItem(template)}
|
||||
aria-label={t('invoiceTemplates.editItem')}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('invoiceTemplates.removeItem')}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${tableStyles.actionBtn} ${tableStyles.actionBtnDanger}`}
|
||||
onClick={() => setRemoveItemTarget(template)}
|
||||
aria-label={t('invoiceTemplates.removeItem')}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className={styles.hint}>{t('invoiceTemplates.hint')}</p>
|
||||
|
||||
<Modal
|
||||
open={itemEditorOpen}
|
||||
title={editingItem ? t('invoiceTemplates.itemModalEditTitle') : t('invoiceTemplates.itemModalAddTitle')}
|
||||
onClose={() => !itemSubmitting && setItemEditorOpen(false)}
|
||||
wide
|
||||
>
|
||||
<div className={formStyles.formGrid}>
|
||||
<div className={`${formStyles.field} ${formStyles.span2}`}>
|
||||
<label htmlFor="item-title">{t('invoiceDraft.fieldTitle')}</label>
|
||||
<input
|
||||
id="item-title"
|
||||
value={itemDraft.title}
|
||||
onChange={(e) => setItemDraft((d) => ({ ...d, title: e.target.value }))}
|
||||
placeholder={t('invoiceDraft.fieldTitlePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className={formStyles.field}>
|
||||
<label htmlFor="item-duration">{t('invoiceDraft.fieldDuration')}</label>
|
||||
<input
|
||||
id="item-duration"
|
||||
value={itemDraft.duration}
|
||||
onChange={(e) => setItemDraft((d) => ({ ...d, duration: e.target.value }))}
|
||||
placeholder={t('invoiceDraft.fieldDurationPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className={formStyles.field}>
|
||||
<label htmlFor="item-worktime">{t('invoiceDraft.fieldWorktime')}</label>
|
||||
<input
|
||||
id="item-worktime"
|
||||
value={itemDraft.worktime}
|
||||
onChange={(e) => setItemDraft((d) => ({ ...d, worktime: e.target.value }))}
|
||||
placeholder={t('invoiceDraft.fieldWorktimePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className={formStyles.field}>
|
||||
<label htmlFor="item-price">{t('invoiceDraft.fieldPrice')}</label>
|
||||
<input
|
||||
id="item-price"
|
||||
inputMode="numeric"
|
||||
value={itemDraft.price}
|
||||
onChange={(e) => setItemDraft((d) => ({ ...d, price: formatIrtInput(e.target.value) }))}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className={formStyles.field}>
|
||||
<label htmlFor="item-discount">{t('invoiceDraft.fieldDiscountedPrice')}</label>
|
||||
<input
|
||||
id="item-discount"
|
||||
inputMode="numeric"
|
||||
value={itemDraft.discountedPrice}
|
||||
onChange={(e) =>
|
||||
setItemDraft((d) => ({ ...d, discountedPrice: formatIrtInput(e.target.value) }))
|
||||
}
|
||||
placeholder={t('invoiceDraft.optional')}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${formStyles.field} ${formStyles.span2}`}>
|
||||
<label htmlFor="item-desc">{t('invoiceDraft.fieldDescription')}</label>
|
||||
<textarea
|
||||
id="item-desc"
|
||||
rows={3}
|
||||
value={itemDraft.description}
|
||||
onChange={(e) => setItemDraft((d) => ({ ...d, description: e.target.value }))}
|
||||
placeholder={t('invoiceDraft.fieldDescriptionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{itemFormError ? <p className={formStyles.alertError}>{itemFormError}</p> : null}
|
||||
<div className={formStyles.actionsRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${formStyles.btn} ${formStyles.btnGhost}`}
|
||||
onClick={() => setItemEditorOpen(false)}
|
||||
disabled={itemSubmitting}
|
||||
>
|
||||
{t('invoiceTemplates.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${formStyles.btn} ${formStyles.btnPrimary}`}
|
||||
onClick={() => void handleSaveItem()}
|
||||
disabled={itemSubmitting}
|
||||
>
|
||||
{itemSubmitting
|
||||
? t('invoiceTemplates.savingItem')
|
||||
: editingItem
|
||||
? t('invoiceTemplates.saveItem')
|
||||
: t('invoiceTemplates.createItem')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
open={!!removeTplTarget}
|
||||
title={t('invoiceTemplates.deleteTemplateTitle')}
|
||||
message={
|
||||
removeTplTarget
|
||||
? t('invoiceTemplates.deleteTemplateMessage', { name: removeTplTarget.name })
|
||||
: ''
|
||||
}
|
||||
onCancel={() => setRemoveTplTarget(null)}
|
||||
onConfirm={() => void handleRemoveTpl()}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
open={!!removeItemTarget}
|
||||
title={t('invoiceTemplates.deleteItemTitle')}
|
||||
message={
|
||||
removeItemTarget
|
||||
? t('invoiceTemplates.deleteItemMessage', { title: removeItemTarget.title })
|
||||
: ''
|
||||
}
|
||||
onCancel={() => setRemoveItemTarget(null)}
|
||||
onConfirm={() => void handleRemoveItem()}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user