import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { FileStack, FileText, Pencil, Plus, Trash2 } from 'lucide-react' import { Breadcrumbs } from '../components/Breadcrumbs' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { EmptyTableRow } from '../components/EmptyState' 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([]) const [invoiceTemplates, setInvoiceTemplates] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [itemEditorOpen, setItemEditorOpen] = useState(false) const [editingItem, setEditingItem] = useState(null) const [itemDraft, setItemDraft] = useState(EMPTY_ITEM_DRAFT) const [itemSubmitting, setItemSubmitting] = useState(false) const [itemFormError, setItemFormError] = useState('') const [removeItemTarget, setRemoveItemTarget] = useState(null) const [removeTplTarget, setRemoveTplTarget] = useState(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 (
{t('title.invoiceTemplates')}

{t('invoiceTemplates.subtitle')}

{error ?

{error}

: null}

{t('invoiceTemplates.section.templates.title')}

{t('invoiceTemplates.section.templates.desc')}

{loading ? t('invoices.loading') : invoiceTemplates.length === 1 ? t('invoiceTemplates.countTemplatesOne', { count: invoiceTemplates.length }) : t('invoiceTemplates.countTemplatesMany', { count: invoiceTemplates.length })}
{!loading && invoiceTemplates.length === 0 ? ( ) : null} {invoiceTemplates.map((template) => ( ))}
{t('invoiceTemplates.col.name')} {t('invoiceTemplates.col.items')} {t('invoiceTemplates.col.keyPoints')} {t('invoiceTemplates.col.accounts')} {t('invoiceTemplates.col.actions')}
{template.name}
{template.topText && !isEmptyRichText(template.topText) ? (
{richTextToPlain(template.topText)}
) : null}
{template.items.length} {template.keyPoints.length} {template.accounts.length}

{t('invoiceTemplates.section.items.title')}

{t('invoiceTemplates.section.items.desc')}

{loading ? t('invoices.loading') : itemTemplates.length === 1 ? t('invoiceTemplates.countItemsOne', { count: itemTemplates.length }) : t('invoiceTemplates.countItemsMany', { count: itemTemplates.length })}
{!loading && itemTemplates.length === 0 ? ( ) : null} {itemTemplates.map((template) => ( ))}
{t('invoiceTemplates.col.title')} {t('invoiceTemplates.col.duration')} {t('invoiceTemplates.col.worktime')} {t('invoiceTemplates.col.price')} {t('invoiceTemplates.col.discounted')} {t('invoiceTemplates.col.actions')}
{template.title}
{template.description ? (
{template.description}
) : null}
{template.duration || '—'} {template.worktime || '—'} {formatIrtPrice(template.price)} {template.discountedPrice === null ? '—' : formatIrtPrice(template.discountedPrice)}

{t('invoiceTemplates.hint')}

!itemSubmitting && setItemEditorOpen(false)} wide >
setItemDraft((d) => ({ ...d, title: e.target.value }))} placeholder={t('invoiceDraft.fieldTitlePlaceholder')} />
setItemDraft((d) => ({ ...d, duration: e.target.value }))} placeholder={t('invoiceDraft.fieldDurationPlaceholder')} />
setItemDraft((d) => ({ ...d, worktime: e.target.value }))} placeholder={t('invoiceDraft.fieldWorktimePlaceholder')} />
setItemDraft((d) => ({ ...d, price: formatIrtInput(e.target.value) }))} placeholder="0" />
setItemDraft((d) => ({ ...d, discountedPrice: formatIrtInput(e.target.value) })) } placeholder={t('invoiceDraft.optional')} />