Official URL migration preserves sessions, shortcuts prefs, and last clock-in client; auth screens match web with legal links; time clock and invoice editor/send flows are updated for the new domain and UI patterns. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import type { AppRouter } from "beenvoice/server/api/root";
|
|
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|
|
|
export type InvoicePdfPreviewInput = inferRouterInputs<AppRouter>["invoices"]["previewPdf"];
|
|
type InvoiceDetail = NonNullable<inferRouterOutputs<AppRouter>["invoices"]["getById"]>;
|
|
|
|
export type InvoicePdfFormFields = {
|
|
invoiceNumber: string;
|
|
invoicePrefix?: string | null;
|
|
businessId?: string | null;
|
|
clientId: string;
|
|
issueDate: Date;
|
|
dueDate: Date;
|
|
status?: "draft" | "sent" | "paid";
|
|
notes?: string | null;
|
|
emailMessage?: string | null;
|
|
taxRate: number;
|
|
currency: string;
|
|
items: Array<{
|
|
date: Date;
|
|
description: string;
|
|
hours: number | string;
|
|
rate: number | string;
|
|
}>;
|
|
};
|
|
|
|
export function buildPreviewPdfInput(fields: InvoicePdfFormFields): InvoicePdfPreviewInput | null {
|
|
if (!fields.clientId.trim()) return null;
|
|
|
|
const items = fields.items.map((item) => ({
|
|
date: item.date,
|
|
description: item.description.trim() || "Service",
|
|
hours: Number(item.hours) || 0,
|
|
rate: Number(item.rate) || 0,
|
|
}));
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
return {
|
|
invoiceNumber: fields.invoiceNumber.trim() || "DRAFT",
|
|
invoicePrefix: fields.invoicePrefix?.trim() || "#",
|
|
businessId: fields.businessId?.trim() || "",
|
|
clientId: fields.clientId,
|
|
issueDate: fields.issueDate,
|
|
dueDate: fields.dueDate,
|
|
status: fields.status ?? "draft",
|
|
notes: fields.notes?.trim() ?? "",
|
|
emailMessage: fields.emailMessage?.trim() ?? "",
|
|
taxRate: fields.taxRate,
|
|
currency: fields.currency,
|
|
items,
|
|
};
|
|
}
|
|
|
|
export function buildPreviewPdfInputFromInvoice(invoice: InvoiceDetail): InvoicePdfPreviewInput {
|
|
return {
|
|
invoiceNumber: invoice.invoiceNumber,
|
|
invoicePrefix: invoice.invoicePrefix ?? "#",
|
|
businessId: invoice.businessId ?? invoice.business?.id ?? "",
|
|
clientId: invoice.clientId,
|
|
issueDate: new Date(invoice.issueDate),
|
|
dueDate: new Date(invoice.dueDate),
|
|
status: invoice.status as "draft" | "sent" | "paid",
|
|
notes: invoice.notes ?? "",
|
|
emailMessage: invoice.emailMessage ?? "",
|
|
taxRate: invoice.taxRate,
|
|
currency: invoice.currency ?? "USD",
|
|
items: invoice.items.map((item) => ({
|
|
date: new Date(item.date),
|
|
description: item.description,
|
|
hours: item.hours,
|
|
rate: item.rate,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function canPreviewPdfInput(input: InvoicePdfPreviewInput | null): input is InvoicePdfPreviewInput {
|
|
if (!input?.clientId) return false;
|
|
return input.items.every((item) => item.description.trim().length > 0);
|
|
}
|