Ship invoice templates, public viewer, and print-ready show page.
Move template/issue editors to full pages, add public invoice SPA with PDF print, and local-aware public URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8f4af7a16c
commit
cbffb23ec3
@@ -0,0 +1,170 @@
|
||||
import { formatIrtInput, parseIrtInput } from '../utils/irtPrice'
|
||||
import type {
|
||||
InvoiceAccountInput,
|
||||
InvoiceItemInput,
|
||||
InvoiceItemTemplate,
|
||||
InvoiceKeyPointInput,
|
||||
InvoiceTemplate,
|
||||
InvoiceTemplateItemInput,
|
||||
} from '../types/invoice'
|
||||
|
||||
export type DraftLineItem = {
|
||||
key: string
|
||||
itemTemplateId?: string
|
||||
title: string
|
||||
duration: string
|
||||
worktime: string
|
||||
description: string
|
||||
price: string
|
||||
discountedPrice: string
|
||||
}
|
||||
|
||||
export type DraftKeyPoint = { key: string; text: string }
|
||||
|
||||
export type DraftAccount = {
|
||||
key: string
|
||||
bankName: string
|
||||
accountHolderName: string
|
||||
cardNumber: string
|
||||
iban: string
|
||||
}
|
||||
|
||||
export function newKey() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
export function emptyDraftItem(): DraftLineItem {
|
||||
return {
|
||||
key: newKey(),
|
||||
title: '',
|
||||
duration: '',
|
||||
worktime: '',
|
||||
description: '',
|
||||
price: '',
|
||||
discountedPrice: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function emptyDraftKeyPoint(): DraftKeyPoint {
|
||||
return { key: newKey(), text: '' }
|
||||
}
|
||||
|
||||
export function emptyDraftAccount(): DraftAccount {
|
||||
return { key: newKey(), bankName: '', accountHolderName: '', cardNumber: '', iban: '' }
|
||||
}
|
||||
|
||||
export function draftItemFromItemTemplate(template: InvoiceItemTemplate): DraftLineItem {
|
||||
return {
|
||||
key: newKey(),
|
||||
itemTemplateId: template.id,
|
||||
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 draftsFromInvoiceTemplate(template: InvoiceTemplate): {
|
||||
name: string
|
||||
topText: string
|
||||
items: DraftLineItem[]
|
||||
keyPoints: DraftKeyPoint[]
|
||||
accounts: DraftAccount[]
|
||||
} {
|
||||
return {
|
||||
name: template.name,
|
||||
topText: template.topText ?? '',
|
||||
items:
|
||||
template.items.length > 0
|
||||
? template.items.map((item) => ({
|
||||
key: newKey(),
|
||||
itemTemplateId: item.itemTemplateId ?? undefined,
|
||||
title: item.title,
|
||||
duration: item.duration ?? '',
|
||||
worktime: item.worktime ?? '',
|
||||
description: item.description ?? '',
|
||||
price: formatIrtInput(String(Math.round(item.price))),
|
||||
discountedPrice:
|
||||
item.discountedPrice === null || item.discountedPrice === undefined
|
||||
? ''
|
||||
: formatIrtInput(String(Math.round(item.discountedPrice))),
|
||||
}))
|
||||
: [emptyDraftItem()],
|
||||
keyPoints:
|
||||
template.keyPoints.length > 0
|
||||
? template.keyPoints.map((kp) => ({ key: newKey(), text: kp.text }))
|
||||
: [],
|
||||
accounts:
|
||||
template.accounts.length > 0
|
||||
? template.accounts.map((acc) => ({
|
||||
key: newKey(),
|
||||
bankName: acc.bankName,
|
||||
accountHolderName: acc.accountHolderName ?? '',
|
||||
cardNumber: acc.cardNumber ?? '',
|
||||
iban: acc.iban ?? '',
|
||||
}))
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function buildLineItemsPayload(items: DraftLineItem[]): InvoiceItemInput[] {
|
||||
return items.map((item) => {
|
||||
const title = item.title.trim()
|
||||
const price = parseIrtInput(item.price)
|
||||
if (!title) throw new Error('Each item needs a title.')
|
||||
if (price === null) throw new Error(`Price is required for “${title}”.`)
|
||||
const discountedPrice = item.discountedPrice.trim()
|
||||
? parseIrtInput(item.discountedPrice)
|
||||
: null
|
||||
if (item.discountedPrice.trim() && discountedPrice === null) {
|
||||
throw new Error(`Discounted price is invalid for “${title}”.`)
|
||||
}
|
||||
if (discountedPrice !== null && discountedPrice > price) {
|
||||
throw new Error(`Discounted price cannot exceed price for “${title}”.`)
|
||||
}
|
||||
return {
|
||||
templateId: item.itemTemplateId,
|
||||
title,
|
||||
duration: item.duration.trim() || undefined,
|
||||
worktime: item.worktime.trim() || undefined,
|
||||
description: item.description.trim() || undefined,
|
||||
price,
|
||||
discountedPrice,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildTemplateItemsPayload(items: DraftLineItem[]): InvoiceTemplateItemInput[] {
|
||||
return buildLineItemsPayload(items).map((item) => ({
|
||||
itemTemplateId: item.templateId,
|
||||
title: item.title,
|
||||
duration: item.duration,
|
||||
worktime: item.worktime,
|
||||
description: item.description,
|
||||
price: item.price,
|
||||
discountedPrice: item.discountedPrice,
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildKeyPointsPayload(points: DraftKeyPoint[]): InvoiceKeyPointInput[] {
|
||||
return points
|
||||
.map((p) => p.text.trim())
|
||||
.filter(Boolean)
|
||||
.map((text) => ({ text }))
|
||||
}
|
||||
|
||||
export function buildAccountsPayload(accounts: DraftAccount[]): InvoiceAccountInput[] {
|
||||
return accounts
|
||||
.map((acc) => ({
|
||||
bankName: acc.bankName.trim(),
|
||||
accountHolderName: acc.accountHolderName.trim() || undefined,
|
||||
cardNumber: acc.cardNumber.trim() || undefined,
|
||||
iban: acc.iban.trim() || undefined,
|
||||
}))
|
||||
.filter((acc) => acc.bankName)
|
||||
}
|
||||
Reference in New Issue
Block a user