Просмотр исходного кода

Customer address on more than one line (#356)

* Take a customer address on more than one line

Street, postcode and town belong on their own lines and print that way on
the invoice, but the field was a single-line input, which refuses a newline
outright.

It is a textarea now, two rows and growing with what is typed, and the
customer page prints what was typed rather than folding it into one line.

The sheet needed nothing: the address passes through to the block as it was
written, the HTML renderer keeps breaks since the shared-sheet fix, and the
PDF's height estimate has always counted them, so the block below is pushed
down rather than overlapped. Tests pin both halves of that. The designer's
stand-in customer now has a two-line address as well, so a workshop can see
what one does to their template before it prints.

* Keep the customer header on one line

The contact row is a summary: company, email, phone and address sitting side
by side under the name. An address broken over three lines there made the
whole header ragged and pushed everything after it down.

The row reads the address back as one line, commas where the breaks were,
with the written form on hover. The invoice, which is where the shape
matters, still prints it as typed.
Bernt Christian Egeland 3 недель назад
Родитель
Сommit
818cda19dc

+ 63 - 0
src/__tests__/features/invoice-designer/multiline-address.test.ts

@@ -0,0 +1,63 @@
+/**
+ * A customer address is written on two or three lines and has to print that
+ * way. The sheet has two renderers with two different ideas of height: the
+ * HTML one measures the rendered DOM, the PDF one estimates from the font
+ * metrics. An estimate that counted a two-line address as one would overlap
+ * the block below it, so both halves are pinned here.
+ */
+import { describe, expect, it } from 'vitest'
+import { buildDocumentSpec } from '@/features/invoice-designer/Spec/buildSpec'
+import { buildSampleData } from '@/features/invoice-designer/Components/sample'
+import { mergeWithDefaults } from '@/features/settings/Schema/invoiceLayoutSchema'
+import { themeOf } from '@/features/invoice-designer/Components/designTheme'
+import { lineCount } from '@/features/invoice-designer/Pdf/measure'
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+const ADDRESS = '12 Harbour Road\nApartment 4\nSpringfield'
+
+function customerBlockText(address: string): string[] {
+  const data = buildSampleData(
+    { name: 'Shop', address: '', phone: '', email: '', logoUrl: null } as any,
+    [],
+    ((key: string) => key) as any,
+    {},
+    'invoice'
+  )
+  data.fields.customer_address = address
+  const layout = mergeWithDefaults({})
+  const spec = buildDocumentSpec(layout, themeOf({} as any, layout), data)
+  const block = spec.blocks.find((b: any) => b.content?.id === 'customer')?.content
+
+  const out: string[] = []
+  const walk = (n: any) => {
+    if (!n || typeof n !== 'object') return
+    if (n.kind === 'text' && n.text) out.push(n.text)
+    for (const child of n.children ?? []) walk(child.node ?? child)
+  }
+  walk(block)
+  return out
+}
+
+describe('a customer address that runs to several lines', () => {
+  it('reaches the sheet with its breaks intact', () => {
+    expect(customerBlockText(ADDRESS)).toContain(ADDRESS)
+  })
+
+  it('is not flattened into one line on the way', () => {
+    const printed = customerBlockText(ADDRESS)
+    expect(printed.some((text) => text.includes('12 Harbour Road Springfield'))).toBe(false)
+  })
+
+  it('is measured as three lines by the PDF, not one', () => {
+    // Wide enough that nothing wraps on its own, so the count is the breaks.
+    expect(lineCount(ADDRESS, 400, undefined, 9)).toBe(3)
+    expect(lineCount('12 Harbour Road', 400, undefined, 9)).toBe(1)
+  })
+
+  it('still measures a long line that has to wrap on its own', () => {
+    // Many short words rather than one long one: the estimator wraps at
+    // spaces and deliberately leaves an over-long word on its own line.
+    const long = 'Harbour Road '.repeat(12)
+    expect(lineCount(long, 60, undefined, 9)).toBeGreaterThan(1)
+  })
+})

+ 15 - 2
src/app/(authenticated)/customers/[id]/customer-detail-client.tsx

@@ -143,6 +143,15 @@ interface SmsMessage {
   toNumber: string
 }
 
+/** An address written on several lines, read back as one. */
+function oneLine(text: string): string {
+  return text
+    .split(/\r?\n/)
+    .map((part) => part.trim())
+    .filter(Boolean)
+    .join(', ')
+}
+
 export function CustomerDetailClient({
   customer,
   customers = [],
@@ -352,8 +361,12 @@ export function CustomerDetailClient({
                 )}
                 {customer.address && (
                   <div className="flex items-center gap-1.5 text-muted-foreground">
-                    <MapPin className="h-3.5 w-3.5" />
-                    <span>{customer.address}</span>
+                    <MapPin className="h-3.5 w-3.5 shrink-0" />
+                    {/* One line here, whatever it was typed as. This row is a
+                        summary sitting beside the email and the phone, and a
+                        three-line address makes the whole header ragged. The
+                        invoice prints it as written. */}
+                    <span title={customer.address}>{oneLine(customer.address)}</span>
                   </div>
                 )}
                 {customer.taxId && (

+ 6 - 1
src/features/customers/Components/CustomerForm.tsx

@@ -275,10 +275,15 @@ export function CustomerForm({
 
             <div className="space-y-2">
               <Label htmlFor="address">{tc('form.address')}</Label>
-              <Input
+              {/* An address is street, postcode and town, and it prints on
+                  the invoice the way it is typed here. One row until there
+                  is more, so the form does not open with a tall empty box. */}
+              <Textarea
                 id="address"
                 name="address"
                 placeholder={t('addressPlaceholder')}
+                rows={2}
+                className="min-h-9 resize-none"
                 defaultValue={customer?.address ?? defaults?.address ?? ''}
               />
             </div>

+ 3 - 1
src/features/invoice-designer/Components/sample.ts

@@ -135,7 +135,9 @@ export function fieldValues(
     // A made-up customer, the same for every workshop.
     customer_name: 'Alex Carter',
     customer_company: 'Carter Logistics Ltd',
-    customer_address: '12 Harbour Road, Springfield',
+    // Two lines, because a customer address is written on two and the
+    // designer should show what that does to the block before it prints.
+    customer_address: '12 Harbour Road\nSpringfield',
     customer_email: 'alex@example.com',
     customer_phone: '+1 555 0134',
     customer_tax_id: `${L('customerTaxId', 'Tax ID')}: 000 000 000`,