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

Write the designer sample's mileage in the workshop's distance unit (#525)

* Write the designer sample's mileage in the workshop's distance unit

* Write the sample observation's distance in the workshop's unit too
Bernt Christian Egeland 1 день назад
Родитель
Сommit
4b1fafbd7b

+ 1 - 1
messages/de/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "Satz",
       "unitEach": "x",
       "findingPads": "Hintere Beläge bei 15 %",
-      "findingPadsNote": "Innerhalb von 5.000 km ersetzen",
+      "findingPadsNote": "{unit, select, mi {Innerhalb von 3.000 Meilen ersetzen} other {Innerhalb von 5.000 km ersetzen}}",
       "findingOilWeep": "Leichter Ölaustritt am Ventildeckel",
       "notes": "Danke, dass Sie unsere Werkstatt gewählt haben. Melden Sie sich bei Fragen gerne.",
       "warranty": "12 Monate Garantie auf Teile und Arbeit.",

+ 1 - 1
messages/en/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "set",
       "unitEach": "x",
       "findingPads": "Rear pads at 15%",
-      "findingPadsNote": "Replace within 5,000 km",
+      "findingPadsNote": "{unit, select, mi {Replace within 3,000 mi} other {Replace within 5,000 km}}",
       "findingOilWeep": "Small oil weep at valve cover",
       "notes": "Thank you for choosing our workshop. Contact us if you have any questions.",
       "warranty": "12 months warranty on parts and labour.",

+ 1 - 1
messages/es/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "juego",
       "unitEach": "x",
       "findingPads": "Pastillas traseras al 15 %",
-      "findingPadsNote": "Sustituir antes de 5.000 km",
+      "findingPadsNote": "{unit, select, mi {Sustituir antes de 3.000 millas} other {Sustituir antes de 5.000 km}}",
       "findingOilWeep": "Ligero rezume de aceite en la tapa de balancines",
       "notes": "Gracias por elegir nuestro taller. Contacta con nosotros si tienes alguna duda.",
       "warranty": "12 meses de garantía en piezas y mano de obra.",

+ 1 - 1
messages/fr/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "jeu",
       "unitEach": "x",
       "findingPads": "Plaquettes arrière à 15 %",
-      "findingPadsNote": "À remplacer sous 5 000 km",
+      "findingPadsNote": "{unit, select, mi {À remplacer sous 3 000 miles} other {À remplacer sous 5 000 km}}",
       "findingOilWeep": "Léger suintement d'huile au cache-culbuteurs",
       "notes": "Merci d'avoir choisi notre atelier. Contactez-nous pour toute question.",
       "warranty": "12 mois de garantie sur les pièces et la main-d'œuvre.",

+ 1 - 1
messages/it/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "kit",
       "unitEach": "x",
       "findingPads": "Pastiglie posteriori al 15%",
-      "findingPadsNote": "Sostituire entro 5.000 km",
+      "findingPadsNote": "{unit, select, mi {Sostituire entro 3.000 miglia} other {Sostituire entro 5.000 km}}",
       "findingOilWeep": "Lieve trafilamento d'olio dal coperchio punterie",
       "notes": "Grazie per aver scelto la nostra officina. Contattaci per qualsiasi domanda.",
       "warranty": "12 mesi di garanzia su ricambi e manodopera.",

+ 1 - 1
messages/lt/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "kompl.",
       "unitEach": "x",
       "findingPads": "Galinės kaladėlės 15 %",
-      "findingPadsNote": "Pakeisti per 5 000 km",
+      "findingPadsNote": "{unit, select, mi {Pakeisti per 3 000 mylių} other {Pakeisti per 5 000 km}}",
       "findingOilWeep": "Nedidelis alyvos sunkimasis ties vožtuvų dangteliu",
       "notes": "Ačiū, kad pasirinkote mūsų servisą. Kilus klausimų, susisiekite su mumis.",
       "warranty": "12 mėnesių garantija dalims ir darbui.",

+ 1 - 1
messages/nb/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "sett",
       "unitEach": "x",
       "findingPads": "Bakre klosser på 15 %",
-      "findingPadsNote": "Skiftes innen 5 000 km",
+      "findingPadsNote": "{unit, select, mi {Skiftes innen 3 000 miles} other {Skiftes innen 5 000 km}}",
       "findingOilWeep": "Liten oljelekkasje ved ventildeksel",
       "notes": "Takk for at du valgte verkstedet vårt. Ta kontakt hvis du har spørsmål.",
       "warranty": "12 måneders garanti på deler og arbeid.",

+ 1 - 1
messages/nl/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "set",
       "unitEach": "x",
       "findingPads": "Achterblokken op 15%",
-      "findingPadsNote": "Binnen 5.000 km vervangen",
+      "findingPadsNote": "{unit, select, mi {Binnen 3.000 mijl vervangen} other {Binnen 5.000 km vervangen}}",
       "findingOilWeep": "Lichte olielekkage bij klepdeksel",
       "notes": "Bedankt dat u voor onze werkplaats hebt gekozen. Neem gerust contact op bij vragen.",
       "warranty": "12 maanden garantie op onderdelen en arbeid.",

+ 1 - 1
messages/pl/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "kpl.",
       "unitEach": "x",
       "findingPads": "Tylne klocki na poziomie 15%",
-      "findingPadsNote": "Wymienić w ciągu 5000 km",
+      "findingPadsNote": "{unit, select, mi {Wymienić w ciągu 3000 mil} other {Wymienić w ciągu 5000 km}}",
       "findingOilWeep": "Lekki wyciek oleju przy pokrywie zaworów",
       "notes": "Dziękujemy za wybór naszego warsztatu. W razie pytań prosimy o kontakt.",
       "warranty": "12 miesięcy gwarancji na części i robociznę.",

+ 1 - 1
messages/pt-BR/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "jogo",
       "unitEach": "x",
       "findingPads": "Pastilhas traseiras em 15%",
-      "findingPadsNote": "Substituir em até 5.000 km",
+      "findingPadsNote": "{unit, select, mi {Substituir em até 3.000 milhas} other {Substituir em até 5.000 km}}",
       "findingOilWeep": "Leve suor de óleo na tampa de válvulas",
       "notes": "Obrigado por escolher nossa oficina. Fale conosco se tiver qualquer dúvida.",
       "warranty": "12 meses de garantia em peças e mão de obra.",

+ 1 - 1
messages/pt-PT/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "jogo",
       "unitEach": "x",
       "findingPads": "Pastilhas traseiras a 15%",
-      "findingPadsNote": "Substituir nos próximos 5000 km",
+      "findingPadsNote": "{unit, select, mi {Substituir nas próximas 3000 milhas} other {Substituir nos próximos 5000 km}}",
       "findingOilWeep": "Ligeira fuga de óleo na tampa das válvulas",
       "notes": "Obrigado por escolher a nossa oficina. Contacte-nos se tiver alguma dúvida.",
       "warranty": "12 meses de garantia em peças e mão de obra.",

+ 1 - 1
messages/ru/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "компл.",
       "unitEach": "x",
       "findingPads": "Задние колодки на 15 %",
-      "findingPadsNote": "Заменить в течение 5 000 км",
+      "findingPadsNote": "{unit, select, mi {Заменить в течение 3 000 миль} other {Заменить в течение 5 000 км}}",
       "findingOilWeep": "Небольшое запотевание масла у клапанной крышки",
       "notes": "Спасибо, что выбрали нашу мастерскую. Если появятся вопросы, свяжитесь с нами.",
       "warranty": "Гарантия 12 месяцев на детали и работу.",

+ 1 - 1
messages/tr/settings.json

@@ -2350,7 +2350,7 @@
       "unitSet": "takım",
       "unitEach": "x",
       "findingPads": "Arka balatalar %15 seviyesinde",
-      "findingPadsNote": "5.000 km içinde değiştirin",
+      "findingPadsNote": "{unit, select, mi {3.000 mil içinde değiştirin} other {5.000 km içinde değiştirin}}",
       "findingOilWeep": "Külbütör kapağında hafif yağ sızıntısı",
       "notes": "Servisimizi seçtiğiniz için teşekkür ederiz. Sorunuz olursa bize ulaşın.",
       "warranty": "Parça ve işçilikte 12 ay garanti.",

+ 62 - 0
src/__tests__/features/invoice-designer/sample-distance-unit.test.ts

@@ -0,0 +1,62 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { IntlMessageFormat } from 'intl-messageformat'
+import { describe, expect, it } from 'vitest'
+import { buildSampleData } from '@/features/invoice-designer/Components/sample'
+import type { DesignerWorkshop } from '@/features/invoice-designer/Components/types'
+
+/**
+ * The sample car's mileage is written in the workshop's own distance unit, the
+ * way its printed invoices write it. A workshop in miles used to preview
+ * "84,120 km" on a sheet that prints miles.
+ */
+
+const t = (key: string) => key
+const labels = { mileage: 'Mileage: {mileage}', km: 'km', mi: 'mi' }
+const workshop: DesignerWorkshop = {
+  name: 'Riverside Auto',
+  address: '',
+  phone: '',
+  email: '',
+  slogan: '',
+  orgNumber: '',
+  paymentTerms: '',
+  logoUrl: '',
+}
+
+const mileage = (unitSystem?: 'metric' | 'imperial') =>
+  buildSampleData({ ...workshop, unitSystem }, [], t, labels, 'invoice').fields.mileage
+
+describe('the sample mileage in the workshop distance unit', () => {
+  it('writes miles for a workshop in miles', () => {
+    expect(mileage('imperial')).toBe('Mileage: 84,120 mi')
+  })
+
+  it('writes kilometres for a workshop in kilometres', () => {
+    expect(mileage('metric')).toBe('Mileage: 84,120 km')
+  })
+
+  it('writes miles when the workshop never chose, as its invoices do', () => {
+    expect(mileage()).toBe('Mileage: 84,120 mi')
+  })
+})
+
+describe('the sample observation in the workshop distance unit', () => {
+  // The real next-intl formatter, so the select in the message is exercised.
+  const messages = JSON.parse(
+    readFileSync(resolve(process.cwd(), 'messages/en/settings.json'), 'utf8')
+  ).designer.sample
+  const intl = (key: string, values?: Record<string, string | number>) =>
+    String(new IntlMessageFormat(messages[key.replace('sample.', '')] ?? key, 'en').format(values))
+  const note = (unitSystem?: 'metric' | 'imperial') =>
+    buildSampleData({ ...workshop, unitSystem }, [], intl, labels, 'invoice').findings[0]?.notes
+
+  it('says miles for a workshop in miles, and when it never chose', () => {
+    expect(note('imperial')).toBe('Replace within 3,000 mi')
+    expect(note()).toBe('Replace within 3,000 mi')
+  })
+
+  it('says kilometres for a workshop in kilometres', () => {
+    expect(note('metric')).toBe('Replace within 5,000 km')
+  })
+})

+ 4 - 0
src/app/(authenticated)/settings/templates/page.tsx

@@ -123,6 +123,10 @@ export default async function TemplatePage() {
     slogan: settings[SETTING_KEYS.WORKSHOP_SLOGAN],
     // Its own taxes too: a split-tax workshop's cards show its GST and QST lines.
     taxComponents: readWorkshopTax(settings).components,
+    // Its distance unit, so the sample car's mileage reads the way its invoices do.
+    unitSystem: (settings[SETTING_KEYS.UNIT_SYSTEM] === 'metric' ? 'metric' : 'imperial') as
+      | 'metric'
+      | 'imperial',
   }
 
   const smsDefaultMap: Record<string, string> = {

+ 1 - 0
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -80,6 +80,7 @@ interface WorkshopPreviewInfo {
   email?: string
   slogan?: string
   taxComponents?: TaxComponentDefinition[] | null
+  unitSystem?: 'metric' | 'imperial'
 }
 
 const fontMap: Record<string, string> = {

+ 2 - 0
src/app/(designer)/invoice-designer/page.tsx

@@ -195,6 +195,8 @@ export default async function InvoiceDesignerPage({
           orgNumberLabel: settings[SETTING_KEYS.ORG_NUMBER_LABEL] || '',
           paymentTerms: settings[SETTING_KEYS.INVOICE_PAYMENT_TERMS] || '',
           logoUrl: settings[SETTING_KEYS.COMPANY_LOGO] || '',
+          // Unset prints miles, as the invoices themselves do.
+          unitSystem: settings[SETTING_KEYS.UNIT_SYSTEM] === 'metric' ? 'metric' : 'imperial',
           signatureUrl,
           taxComponents: readWorkshopTax(settings).components,
         }}

+ 3 - 0
src/features/invoice-designer/Components/PresetPreview.tsx

@@ -29,6 +29,8 @@ interface PreviewWorkshop {
   slogan?: string
   /** The workshop's split tax, so a card shows its GST and QST lines. */
   taxComponents?: TaxComponentDefinition[] | null
+  /** How the workshop writes distance, for the sample car's mileage. */
+  unitSystem?: 'metric' | 'imperial'
 }
 
 /** The sample document on this workshop's own details, for a card's picture. */
@@ -60,6 +62,7 @@ function useSampleData(docType: DocumentType, workshop?: PreviewWorkshop, logoUr
         typeKeyEnabled,
         logoUrl: logoUrl ?? '',
         taxComponents: workshop?.taxComponents ?? null,
+        unitSystem: workshop?.unitSystem,
       },
       [],
       t,

+ 7 - 4
src/features/invoice-designer/Components/sample.ts

@@ -88,7 +88,9 @@ export interface SampleTables {
 export function sampleTables(
   t: SampleT,
   labels: PrintLabels,
-  format: SampleFormat = DEFAULT_SAMPLE_FORMAT
+  format: SampleFormat = DEFAULT_SAMPLE_FORMAT,
+  /** The workshop's distance unit, for the one observation that names a distance. */
+  unitSystem: DesignerWorkshop['unitSystem'] = 'imperial'
 ): SampleTables {
   const L = (key: string, fallback: string) => labels[key] || fallback
   const m = format.money
@@ -144,7 +146,7 @@ export function sampleTables(
         severity: L('findingSeverityNeedsWork', 'Medium'),
         color: '#f59e0b',
         description: t('sample.findingPads'),
-        notes: t('sample.findingPadsNote'),
+        notes: t('sample.findingPadsNote', { unit: unitSystem === 'metric' ? 'km' : 'mi' }),
       },
       {
         severity: L('findingSeverityMonitor', 'Low'),
@@ -198,7 +200,8 @@ export function fieldValues(
       ? fillTemplate(L('typeKey', 'HSN/TSN: {typeKey}'), { typeKey: '9101 / ABC' })
       : '',
     mileage: fillTemplate(L('mileage', 'Mileage: {mileage}'), {
-      mileage: `84,120 ${L('km', 'km')}`,
+      // In the workshop's unit; unset prints miles, as its invoices do.
+      mileage: `84,120 ${workshop.unitSystem === 'metric' ? L('km', 'km') : L('mi', 'mi')}`,
     }),
     // A made-up job.
     service_title: t('sample.serviceTitle'),
@@ -322,7 +325,7 @@ export function buildSampleData(
   format: SampleFormat = DEFAULT_SAMPLE_FORMAT
 ): DocumentData {
   const L = (key: string, fallback: string) => labels[key] || fallback
-  const sample = sampleTables(t, labels, format)
+  const sample = sampleTables(t, labels, format, workshop.unitSystem)
   const values = fieldValues(workshop, t, labels)
   const tax = sampleTax(workshop, sample, labels, format)
   return {

+ 2 - 0
src/features/invoice-designer/Components/types.ts

@@ -62,6 +62,8 @@ export interface DesignerWorkshop {
   paymentTerms: string
   /** Whether the workshop records the German type key, so the sample car shows one. */
   typeKeyEnabled?: boolean
+  /** How the workshop writes distance; the sample car's mileage uses it, as printed invoices do. */
+  unitSystem?: 'metric' | 'imperial'
   logoUrl: string
   /**
    * The signature of whoever has the designer open, so the Signature section