Преглед изворни кода

Show the owner and the job total in the import dry run (#532)

Bernt Christian Egeland пре 1 дан
родитељ
комит
d2b8306644

+ 138 - 0
src/__tests__/features/import/dry-run-table.test.tsx

@@ -0,0 +1,138 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen, within } from '@testing-library/react'
+import { ImportWizard } from '@/features/import/Components/ImportWizard'
+import { type ExistingData, type ImportOptions, planImport } from '@/features/import/Lib/pipeline'
+import type { ColumnMapping } from '@/features/import/Lib/suggest'
+import type { AnalyzeResponse } from '@/features/import/Lib/types'
+import { CurrencySettingsProvider } from '@/components/currency-settings-context'
+
+/**
+ * The dry run table says who owns each vehicle and what each past job cost
+ * the way the rest of the app does. A vehicle file that names its owner by
+ * email showed "-" under Owner on every row, though each row was linked to a
+ * customer, and job totals showed as bare numbers.
+ */
+
+const existing: ExistingData = {
+  customers: [
+    {
+      id: 'c1',
+      name: 'Laura Castillo',
+      customerNumber: null,
+      email: 'laura@example.com',
+      phone: null,
+    },
+  ],
+  vehicles: [
+    {
+      id: 'v1',
+      make: 'Ford',
+      model: 'F-150',
+      year: 2019,
+      vin: null,
+      licensePlate: 'BKT-4407',
+      customerId: 'c1',
+    },
+  ],
+  invoiceNumbers: [],
+}
+
+let file: { columns: string[]; rows: string[][]; mapping: ColumnMapping }
+
+vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }) }))
+vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
+vi.mock('@/features/import/Actions/importActions', () => ({
+  dryRunImport: vi.fn(async (input: { options: ImportOptions }) => {
+    const plan = planImport(file.rows, file.mapping, input.options, existing)
+    return {
+      success: true,
+      data: {
+        summary: plan.summary,
+        limit: null,
+        issues: [],
+        page: { rows: plan.rows, total: plan.rows.length, page: 0, pageSize: 100 },
+      },
+    }
+  }),
+  commitImport: vi.fn(),
+  getImportPlanPage: vi.fn(),
+  getImportProgress: vi.fn(),
+  getImportReportCsv: vi.fn(),
+  suggestMappingWithAi: vi.fn(),
+  undoImportBatch: vi.fn(),
+}))
+
+const analyze = (entity: string): AnalyzeResponse => ({
+  token: 't',
+  fileName: `${entity}.csv`,
+  format: 'csv',
+  sheetName: null,
+  delimiter: ',',
+  encoding: 'UTF-8',
+  columns: file.columns,
+  sampleRows: file.rows.slice(0, 3),
+  totalRows: file.rows.length,
+  suggestion: {
+    mapping: file.mapping,
+    source: {},
+    presetId: null,
+    dateFormat: 'MDY',
+    decimalSeparator: '.',
+  },
+  fields: [],
+  presets: [],
+  defaults: { countryCode: '+1' },
+  aiAvailable: false,
+})
+
+async function dryRun(entity: 'vehicles' | 'services'): Promise<void> {
+  globalThis.fetch = vi.fn(
+    async () => new Response(JSON.stringify(analyze(entity)))
+  ) as typeof fetch
+  const { container } = render(
+    <CurrencySettingsProvider currencyCode="USD" currencyFormat="symbol">
+      <ImportWizard open onOpenChange={vi.fn()} entity={entity} lockEntity />
+    </CurrencySettingsProvider>
+  )
+  const input = container.ownerDocument.querySelector('input[type="file"]') as HTMLInputElement
+  fireEvent.change(input, { target: { files: [new File(['x'], `${entity}.csv`)] } })
+  fireEvent.click(await screen.findByRole('button', { name: /run dry run/i }))
+  await screen.findByText(/will be created/i)
+}
+
+describe('the import dry run table', () => {
+  const realFetch = globalThis.fetch
+  beforeEach(() => vi.clearAllMocks())
+  afterEach(() => {
+    globalThis.fetch = realFetch
+  })
+
+  it('shows the customer a vehicle is linked to when the file names the owner by email', async () => {
+    file = {
+      columns: ['Owner E-mail', 'Vehicle', 'Tag'],
+      rows: [['laura@example.com', '2021 Toyota Camry', 'HWB-4362']],
+      mapping: { '0': 'customer.email', '1': 'vehicle.description', '2': 'vehicle.licensePlate' },
+    }
+    await dryRun('vehicles')
+    const row = (await screen.findByText('2021 Toyota Camry')).closest('tr') as HTMLElement
+    const cells = within(row).getAllByRole('cell')
+    // Columns: #, action, vehicle, plate, owner, details.
+    expect(cells[4]).toHaveTextContent('Laura Castillo')
+  })
+
+  it('shows what each past job cost as money', async () => {
+    file = {
+      columns: ['Date', 'Tag', 'Job', 'Total'],
+      rows: [['03/14/2025', 'BKT-4407', 'State inspection', '$25.00']],
+      mapping: {
+        '0': 'service.date',
+        '1': 'vehicle.licensePlate',
+        '2': 'service.title',
+        '3': 'service.total',
+      },
+    }
+    await dryRun('services')
+    const row = (await screen.findByText('State inspection')).closest('tr') as HTMLElement
+    expect(within(row).getByText('$25.00')).toBeInTheDocument()
+  })
+})

+ 12 - 4
src/features/import/Components/ImportWizard.tsx

@@ -30,6 +30,7 @@ import {
   DialogTitle,
   DialogTitle,
 } from '@/components/ui/dialog'
 } from '@/components/ui/dialog'
 import { DocsLink } from '@/components/docs-link'
 import { DocsLink } from '@/components/docs-link'
+import { useFormatCurrency } from '@/components/currency-settings-context'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import { SearchableSelect, type SearchableOption } from '@/components/ui/searchable-select'
 import { SearchableSelect, type SearchableOption } from '@/components/ui/searchable-select'
@@ -1053,6 +1054,7 @@ function PreviewRow({
   onAction: (action: RowAction) => void
   onAction: (action: RowAction) => void
 }) {
 }) {
   const t = useTranslations('dataImport')
   const t = useTranslations('dataImport')
+  const formatCurrency = useFormatCurrency()
   const matched =
   const matched =
     entity === 'customers'
     entity === 'customers'
       ? row.customerMatch
       ? row.customerMatch
@@ -1129,7 +1131,13 @@ function PreviewRow({
             {row.vehicle?.licensePlate ?? row.vehicle?.vin ?? '-'}
             {row.vehicle?.licensePlate ?? row.vehicle?.vin ?? '-'}
           </TableCell>
           </TableCell>
           <TableCell className="whitespace-nowrap text-sm text-muted-foreground">
           <TableCell className="whitespace-nowrap text-sm text-muted-foreground">
-            {row.customer?.name ?? '-'}
+            {/* Files often name the owner by email or phone only; the customer it
+                links to is then the owner to show. */}
+            {row.customer?.name ??
+              row.customerMatch?.name ??
+              row.customer?.email ??
+              row.customer?.phone ??
+              '-'}
           </TableCell>
           </TableCell>
         </>
         </>
       )}
       )}
@@ -1142,12 +1150,12 @@ function PreviewRow({
             {row.service?.title ?? '-'}
             {row.service?.title ?? '-'}
           </TableCell>
           </TableCell>
           <TableCell className="whitespace-nowrap text-sm text-muted-foreground">
           <TableCell className="whitespace-nowrap text-sm text-muted-foreground">
-            {row.vehicleMatch?.label ??
-              [vehicle, row.vehicle?.licensePlate].filter(Boolean).join(' ') ??
+            {row.vehicleMatch?.label ||
+              [vehicle, row.vehicle?.licensePlate].filter(Boolean).join(' ') ||
               '-'}
               '-'}
           </TableCell>
           </TableCell>
           <TableCell className="whitespace-nowrap text-right text-sm tabular-nums">
           <TableCell className="whitespace-nowrap text-right text-sm tabular-nums">
-            {row.service?.total ?? '-'}
+            {row.service?.total != null ? formatCurrency(row.service.total) : '-'}
           </TableCell>
           </TableCell>
         </>
         </>
       )}
       )}