feat(customers): let users set a customer number shown on the invoice (#957)
Implements #914 (kundnummer on customers, printed on the invoice PDF). - Migration: nullable text column customers.customer_number, no unique constraint in v1 so existing rows and imports keep working. - API: CreateCustomerSchema/UpdateCustomerSchema accept an optional customer_number (trimmed, max 32 chars, nullable-then-optional so the OpenAPI registry sees it as not required); create/update routes persist it and normalize empty string to null so it can be cleared. - v1 public API: customers create/detail/update round-trip the field (insert and update field lists, response projections, response schemas), and the invoices :send route fetches customer_number in its explicit customer join so the emailed PDF matches the downloaded one (the pdf route already selects customers(*)). - UI: optional Kundnummer field in CustomerForm (next-intl keys in both sv and en), wired into the edit dialog's initialData; read-only Kundnummer row on the customer detail page's business-details card. - Invoice PDF: renders "Kundnr:" / "Customer no.:" in the customer box when set; the PDF reads the live customers join, so no snapshot column is needed. - Tests: route tests cover 400 validation, trimming, clearing with null/empty, and omit-leaves-untouched on POST and PATCH; v1 tests cover the create/update round-trip (insert/update payload + response projection) and the :send customer-join projection. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
11400b4dec
commit
2e7931b36b
@@ -38,6 +38,7 @@ export default function CustomerForm({
|
||||
const schema = useMemo(() => z.object({
|
||||
name: z.string().min(1, t('name_required')),
|
||||
customer_type: z.enum(['individual', 'swedish_business', 'eu_business', 'non_eu_business']),
|
||||
customer_number: z.string().trim().max(32, t('customer_number_too_long')).optional(),
|
||||
email: z.string().email(t('email_invalid')).optional().or(z.literal('')),
|
||||
phone: z.string().optional(),
|
||||
address_line1: z.string().optional(),
|
||||
@@ -70,6 +71,7 @@ export default function CustomerForm({
|
||||
defaultValues: {
|
||||
name: initialData?.name || '',
|
||||
customer_type: initialData?.customer_type || 'swedish_business',
|
||||
customer_number: initialData?.customer_number || '',
|
||||
email: initialData?.email || '',
|
||||
phone: initialData?.phone || '',
|
||||
address_line1: initialData?.address_line1 || '',
|
||||
@@ -178,6 +180,21 @@ export default function CustomerForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Customer number */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="customer_number">{t('customer_number_label')}</Label>
|
||||
<Input
|
||||
id="customer_number"
|
||||
placeholder={t('customer_number_placeholder')}
|
||||
{...register('customer_number')}
|
||||
/>
|
||||
{errors.customer_number ? (
|
||||
<p className="text-sm text-destructive">{errors.customer_number.message}</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">{t('customer_number_hint')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user