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:
Alireza Hassani
2026-08-11 15:17:03 +03:30
co-authored by Cursor
parent 722933dda9
commit 917b840ee5
44 changed files with 5136 additions and 105 deletions
@@ -0,0 +1,159 @@
import { apiRequest } from '../lib/api'
import { getActiveBusinessId } from '../lib/businessContext'
import type {
CreateInvoiceItemTemplatePayload,
CreateInvoicePayload,
CreateInvoiceTemplatePayload,
Invoice,
InvoiceItemTemplate,
InvoiceItemTemplatesResponse,
InvoiceStatus,
InvoiceTemplate,
InvoiceTemplatesListResponse,
InvoicesListResponse,
UpdateInvoiceItemTemplatePayload,
UpdateInvoicePayload,
UpdateInvoiceTemplatePayload,
} from '../types/invoice'
function businessPath(suffix = '') {
const businessId = getActiveBusinessId()
if (!businessId) {
throw new Error('No active business selected. Please sign in again.')
}
return `/businesses/${businessId}${suffix}`
}
export function listInvoiceItemTemplates(signal?: AbortSignal) {
return apiRequest<InvoiceItemTemplatesResponse>(businessPath('/invoice-item-templates'), {
auth: true,
signal,
})
}
export function createInvoiceItemTemplate(payload: CreateInvoiceItemTemplatePayload) {
return apiRequest<InvoiceItemTemplate>(businessPath('/invoice-item-templates'), {
method: 'POST',
auth: true,
body: payload,
})
}
export function updateInvoiceItemTemplate(
templateId: string,
payload: UpdateInvoiceItemTemplatePayload,
) {
return apiRequest<InvoiceItemTemplate>(
businessPath(`/invoice-item-templates/${templateId}`),
{
method: 'PATCH',
auth: true,
body: payload,
},
)
}
export function deleteInvoiceItemTemplate(templateId: string) {
return apiRequest<{ ok: boolean }>(businessPath(`/invoice-item-templates/${templateId}`), {
method: 'DELETE',
auth: true,
})
}
export function listInvoiceTemplates(signal?: AbortSignal) {
return apiRequest<InvoiceTemplatesListResponse>(businessPath('/invoice-templates'), {
auth: true,
signal,
})
}
export function getInvoiceTemplate(templateId: string, signal?: AbortSignal) {
return apiRequest<InvoiceTemplate>(businessPath(`/invoice-templates/${templateId}`), {
auth: true,
signal,
})
}
export function createInvoiceTemplate(payload: CreateInvoiceTemplatePayload) {
return apiRequest<InvoiceTemplate>(businessPath('/invoice-templates'), {
method: 'POST',
auth: true,
body: payload,
})
}
export function updateInvoiceTemplate(templateId: string, payload: UpdateInvoiceTemplatePayload) {
return apiRequest<InvoiceTemplate>(businessPath(`/invoice-templates/${templateId}`), {
method: 'PATCH',
auth: true,
body: payload,
})
}
export function deleteInvoiceTemplate(templateId: string) {
return apiRequest<{ ok: boolean }>(businessPath(`/invoice-templates/${templateId}`), {
method: 'DELETE',
auth: true,
})
}
export interface ListInvoicesParams {
page?: number
pageSize?: number
status?: InvoiceStatus
userId?: string
}
export function listInvoices(params: ListInvoicesParams = {}, signal?: AbortSignal) {
const search = new URLSearchParams()
if (params.page) search.set('page', String(params.page))
if (params.pageSize) search.set('pageSize', String(params.pageSize))
if (params.status) search.set('status', params.status)
if (params.userId) search.set('userId', params.userId)
const qs = search.toString()
return apiRequest<InvoicesListResponse>(businessPath(`/invoices${qs ? `?${qs}` : ''}`), {
auth: true,
signal,
})
}
export function getInvoice(invoiceId: string, signal?: AbortSignal) {
return apiRequest<Invoice>(businessPath(`/invoices/${invoiceId}`), {
auth: true,
signal,
})
}
export function createInvoice(payload: CreateInvoicePayload) {
return apiRequest<Invoice>(businessPath('/invoices'), {
method: 'POST',
auth: true,
body: payload,
})
}
export function updateInvoice(invoiceId: string, payload: UpdateInvoicePayload) {
return apiRequest<Invoice>(businessPath(`/invoices/${invoiceId}`), {
method: 'PUT',
auth: true,
body: payload,
})
}
export function updateInvoiceStatus(
invoiceId: string,
payload: { status: InvoiceStatus; notes?: string },
) {
return apiRequest<Invoice>(businessPath(`/invoices/${invoiceId}`), {
method: 'PATCH',
auth: true,
body: payload,
})
}
export function deleteInvoice(invoiceId: string) {
return apiRequest<{ ok: boolean }>(businessPath(`/invoices/${invoiceId}`), {
method: 'DELETE',
auth: true,
})
}