import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { FileText, Pencil, Plus, Settings as SettingsIcon, Trash2 } from 'lucide-react' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { Modal } from '../components/Modal' import { useToast } from '../context/ToastContext' import { ApiError, isAbortError } from '../lib/api' import { createInvoiceItemTemplate, deleteInvoiceItemTemplate, listInvoiceItemTemplates, updateInvoiceItemTemplate, } from '../services/invoiceService' import type { InvoiceItemTemplate } from '../types/invoice' import { formatIrtInput, formatIrtPrice, parseIrtInput } from '../utils/irtPrice' import pageStyles from '../components/PageContent.module.css' import tableStyles from './BusinessesPage.module.css' import styles from './SettingsPage.module.css' type TemplateDraft = { title: string duration: string worktime: string description: string price: string discountedPrice: string } const EMPTY_DRAFT: TemplateDraft = { title: '', duration: '', worktime: '', description: '', price: '', discountedPrice: '', } function draftFromTemplate(t: InvoiceItemTemplate): TemplateDraft { return { title: t.title, duration: t.duration ?? '', worktime: t.worktime ?? '', description: t.description ?? '', price: formatIrtInput(String(Math.round(t.price))), discountedPrice: t.discountedPrice === null || t.discountedPrice === undefined ? '' : formatIrtInput(String(Math.round(t.discountedPrice))), } } function toPayload(draft: TemplateDraft) { const price = parseIrtInput(draft.price) if (!draft.title.trim()) { throw new Error('Title is required.') } if (price === null) { throw new Error('Price is required.') } const discountedPrice = draft.discountedPrice.trim() ? parseIrtInput(draft.discountedPrice) : null if (draft.discountedPrice.trim() && discountedPrice === null) { throw new Error('Discounted price is invalid.') } if (discountedPrice !== null && discountedPrice > price) { throw new Error('Discounted price cannot exceed price.') } return { title: draft.title.trim(), duration: draft.duration.trim() || undefined, worktime: draft.worktime.trim() || undefined, description: draft.description.trim() || undefined, price, discountedPrice, } } export function SettingsPage() { const { showToast } = useToast() const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [editorOpen, setEditorOpen] = useState(false) const [editing, setEditing] = useState(null) const [draft, setDraft] = useState(EMPTY_DRAFT) const [submitting, setSubmitting] = useState(false) const [formError, setFormError] = useState('') const [removeTarget, setRemoveTarget] = useState(null) async function reload(signal?: AbortSignal) { setLoading(true) setError('') try { const result = await listInvoiceItemTemplates(signal) setTemplates(result.items) } catch (err) { if (isAbortError(err)) return setError(err instanceof ApiError ? err.message : 'Unable to load invoice templates.') } finally { setLoading(false) } } useEffect(() => { const controller = new AbortController() void reload(controller.signal) return () => controller.abort() }, []) function openCreate() { setEditing(null) setDraft(EMPTY_DRAFT) setFormError('') setEditorOpen(true) } function openEdit(template: InvoiceItemTemplate) { setEditing(template) setDraft(draftFromTemplate(template)) setFormError('') setEditorOpen(true) } async function handleSave() { setFormError('') let payload try { payload = toPayload(draft) } catch (err) { setFormError(err instanceof Error ? err.message : 'Invalid form.') return } setSubmitting(true) try { if (editing) { await updateInvoiceItemTemplate(editing.id, payload) showToast('Invoice item updated.', 'success') } else { await createInvoiceItemTemplate(payload) showToast('Invoice item created.', 'success') } setEditorOpen(false) await reload() } catch (err) { setFormError(err instanceof ApiError ? err.message : 'Unable to save invoice item.') } finally { setSubmitting(false) } } async function handleRemove() { if (!removeTarget) return try { await deleteInvoiceItemTemplate(removeTarget.id) showToast('Invoice item removed.', 'success') setRemoveTarget(null) await reload() } catch (err) { showToast(err instanceof ApiError ? err.message : 'Unable to remove item.', 'error') } } return (

Settings

Platform configuration used across Meshkee super admin tools.

Invoices

Predefined line items you can reuse when issuing invoices to businesses.

{error ?

{error}

: null}
Predefined invoice items
{loading ? 'Loading…' : `${templates.length} item${templates.length === 1 ? '' : 's'}`}
{!loading && templates.length === 0 ? ( ) : null} {templates.map((template) => ( ))}
Title Duration Worktime Price Discounted Actions
No predefined items yet. Add one to speed up invoice creation.
{template.title}
{template.description ? (
{template.description}
) : null}
{template.duration || '—'} {template.worktime || '—'} {formatIrtPrice(template.price)} {template.discountedPrice === null ? '—' : formatIrtPrice(template.discountedPrice)}

Tip: open a business from Businesses to list invoices or issue a new one.

!submitting && setEditorOpen(false)} wide >
setDraft((d) => ({ ...d, title: e.target.value }))} placeholder="e.g. Website setup" />
setDraft((d) => ({ ...d, duration: e.target.value }))} placeholder="e.g. 3 months" />
setDraft((d) => ({ ...d, worktime: e.target.value }))} placeholder="e.g. 40 hours" />
setDraft((d) => ({ ...d, price: formatIrtInput(e.target.value) }))} placeholder="0" />
setDraft((d) => ({ ...d, discountedPrice: formatIrtInput(e.target.value) })) } placeholder="Optional" />