Kaynağa Gözat

Add a way to add a status from the work order's status menu (#457)

Bernt Christian Egeland 2 hafta önce
ebeveyn
işleme
959ecb3e81

+ 5 - 0
e2e/specs/work-orders/modern-page.spec.ts

@@ -479,6 +479,11 @@ test.describe('the overhauled work order page', () => {
           timeout: 2_000,
         })
       }).toPass({ timeout: 30_000 })
+      // The menu ends with the way to add a status, for somebody who may.
+      await expect(page.getByTestId('status-add-in-progress')).toHaveAttribute(
+        'href',
+        '/settings/work-order-statuses'
+      )
       await page.getByRole('menuitem', { name: 'Waiting Parts' }).click()
       await expectStatus(page, /Waiting Parts/)
       // The status says "Saved" for two seconds. Let that one go, or it answers

+ 1 - 0
messages/de/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Kunde seit"
     },
     "moreStatuses": "Weitere Status",
+    "addStatus": "Status hinzufügen",
     "fromInspection": "Aus Inspektion {name}, {date}",
     "fromQuote": "Aus Angebot {ref}",
     "startInspection": "Inspektion starten"

+ 1 - 0
messages/en/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Customer since"
     },
     "moreStatuses": "More statuses",
+    "addStatus": "Add status",
     "fromInspection": "From inspection {name}, {date}",
     "fromQuote": "From quote {ref}",
     "startInspection": "Start inspection"

+ 1 - 0
messages/es/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Cliente desde"
     },
     "moreStatuses": "Más estados",
+    "addStatus": "Añadir estado",
     "fromInspection": "De la inspección {name}, {date}",
     "fromQuote": "Del presupuesto {ref}",
     "startInspection": "Iniciar inspección"

+ 1 - 0
messages/fr/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Client depuis"
     },
     "moreStatuses": "Autres statuts",
+    "addStatus": "Ajouter un statut",
     "fromInspection": "Issu de l'inspection {name}, {date}",
     "fromQuote": "Issu du devis {ref}",
     "startInspection": "Commencer l'inspection"

+ 1 - 0
messages/it/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Cliente dal"
     },
     "moreStatuses": "Altri stati",
+    "addStatus": "Aggiungi stato",
     "fromInspection": "Dall'ispezione {name}, {date}",
     "fromQuote": "Dal preventivo {ref}",
     "startInspection": "Avvia ispezione"

+ 1 - 0
messages/lt/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Klientas nuo"
     },
     "moreStatuses": "Daugiau būsenų",
+    "addStatus": "Pridėti būseną",
     "fromInspection": "Iš apžiūros {name}, {date}",
     "fromQuote": "Iš pasiūlymo {ref}",
     "startInspection": "Pradėti apžiūrą"

+ 1 - 0
messages/nb/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Kunde siden"
     },
     "moreStatuses": "Flere statuser",
+    "addStatus": "Legg til status",
     "fromInspection": "Fra inspeksjon {name}, {date}",
     "fromQuote": "Fra tilbud {ref}",
     "startInspection": "Start inspeksjon"

+ 1 - 0
messages/nl/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Klant sinds"
     },
     "moreStatuses": "Meer statussen",
+    "addStatus": "Status toevoegen",
     "fromInspection": "Uit inspectie {name}, {date}",
     "fromQuote": "Uit offerte {ref}",
     "startInspection": "Inspectie starten"

+ 1 - 0
messages/pl/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Klient od"
     },
     "moreStatuses": "Więcej statusów",
+    "addStatus": "Dodaj status",
     "fromInspection": "Z przeglądu {name}, {date}",
     "fromQuote": "Z wyceny {ref}",
     "startInspection": "Rozpocznij przegląd"

+ 1 - 0
messages/pt-BR/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Cliente desde"
     },
     "moreStatuses": "Mais status",
+    "addStatus": "Adicionar status",
     "fromInspection": "Da inspeção {name}, {date}",
     "fromQuote": "Do orçamento {ref}",
     "startInspection": "Iniciar inspeção"

+ 1 - 0
messages/pt-PT/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Cliente desde"
     },
     "moreStatuses": "Mais estados",
+    "addStatus": "Adicionar estado",
     "fromInspection": "Da inspeção {name}, {date}",
     "fromQuote": "Do orçamento {ref}",
     "startInspection": "Iniciar inspeção"

+ 1 - 0
messages/ru/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Клиент с"
     },
     "moreStatuses": "Другие статусы",
+    "addStatus": "Добавить статус",
     "fromInspection": "Из осмотра {name}, {date}",
     "fromQuote": "Из предложения {ref}",
     "startInspection": "Начать осмотр"

+ 1 - 0
messages/tr/service.json

@@ -668,6 +668,7 @@
       "customerSince": "Müşteri olma tarihi"
     },
     "moreStatuses": "Diğer durumlar",
+    "addStatus": "Durum ekle",
     "fromInspection": "{name} muayenesinden, {date}",
     "fromQuote": "{ref} teklifinden",
     "startInspection": "Muayeneyi başlat"

+ 2 - 0
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -84,6 +84,7 @@ export function ServicePageClient({
   tireHotelEnabled = false,
   onlinePayments = false,
   workOrderStatuses = [],
+  canAddStatus = false,
   videoCall = { link: null, providers: [] },
   tireThresholds,
   defaultTaxRate,
@@ -689,6 +690,7 @@ export function ServicePageClient({
                 {...leftColumnProps}
                 {...rightColumnProps}
                 workOrderStatuses={workOrderStatuses}
+                canAddStatus={canAddStatus}
                 onNotifyForStatus={handleNotifyForStatus}
                 locked={lockState.locked}
                 lockedLabel={t('invoice.lockedFieldsetLabel')}

+ 5 - 0
src/features/vehicles/Components/service-page/ServiceRecordPage.tsx

@@ -40,6 +40,7 @@ import {
 } from '@/features/condition-map/Lib/loadMarks.server'
 import { invoiceDesignPrintsConditionMap } from '@/features/invoices/Lib/assembleInvoicePrint'
 import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
+import { canEditSettings } from '@/lib/settings-access'
 import { ServicePageClient } from '@/features/vehicles/Components/service-page/ServicePageClient'
 import { listDesignOptions } from '@/features/invoice-designer/Actions/documentDesignActions'
 import { rendersFromIssue } from '@/features/invoices/Lib/issuedInvoice'
@@ -175,6 +176,9 @@ export async function ServiceRecordPage({
   // Fetch team members and features
   const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null
   const orgId = membership?.organizationId
+  // Whether the status menus offer the way to add a status. Somebody who
+  // cannot change settings is better off not seeing a link that turns them away.
+  const canAddStatus = await canEditSettings(session?.user?.id ?? '', membership?.role)
 
   const [currentUser, features, aiProvider, dictation, tireHotel] = await Promise.all([
     session?.user?.id
@@ -437,6 +441,7 @@ export async function ServiceRecordPage({
         dictationMode={dictation.mode}
         tireHotelEnabled={tireHotel.enabled}
         onlinePayments={onlinePayments}
+        canAddStatus={canAddStatus}
         workOrderStatuses={
           workOrderStatusesResult.success && workOrderStatusesResult.data
             ? workOrderStatusesResult.data

+ 3 - 0
src/features/vehicles/Components/service-page/modern/ModernDetails.tsx

@@ -45,6 +45,8 @@ type RightProps = ComponentProps<typeof DetailsRightColumn>
 export interface ModernDetailsProps extends LeftProps, Omit<RightProps, keyof LeftProps> {
   /** The workshop's own statuses, for the menus on the stepper. */
   workOrderStatuses?: WorkOrderStatusOption[]
+  /** The viewer may change settings, so the status menus offer to add a status. */
+  canAddStatus?: boolean
   /** Offers to tell the customer about a status that asks for it. */
   onNotifyForStatus?: (status: WorkOrderStatusOption) => void
   /**
@@ -292,6 +294,7 @@ export function ModernDetails(props: ModernDetailsProps) {
               status={formState.status}
               customStatus={customStatus}
               options={props.workOrderStatuses}
+              canAddStatus={props.canAddStatus}
               onChange={(next, custom) => void changeStatus(next, custom)}
             />
 

+ 29 - 1
src/features/vehicles/Components/service-page/modern/StatusStepper.tsx

@@ -1,7 +1,8 @@
 'use client'
 
+import Link from 'next/link'
 import { useTranslations } from 'next-intl'
-import { Check, ChevronDown } from 'lucide-react'
+import { Check, ChevronDown, Plus } from 'lucide-react'
 import {
   DropdownMenu,
   DropdownMenuContent,
@@ -38,12 +39,16 @@ type Chosen = { id: string; name: string; color: string } | null
  * A stage with nothing under it has no arrow, so a workshop that defines no
  * statuses sees three stages and one arrow, and nothing else changes for it.
  *
+ * Each menu ends with the way to add a status, for somebody who may change
+ * settings: the menu is where the missing status is noticed.
+ *
  * It sits outside the form's lock: a locked invoice still moves along.
  */
 export function StatusStepper({
   status,
   customStatus = null,
   options = [],
+  canAddStatus = false,
   onChange,
 }: {
   status: string
@@ -51,6 +56,8 @@ export function StatusStepper({
   customStatus?: { id: string; name: string; color: string } | null
   /** The workshop's statuses that can be chosen. */
   options?: WorkOrderStatusOption[]
+  /** The viewer may change settings, so each menu ends with the way to add a status. */
+  canAddStatus?: boolean
   /** A stage alone, or a stage with one of the workshop's statuses under it. */
   onChange: (status: string, custom: WorkOrderStatusOption | null) => void
 }) {
@@ -142,6 +149,7 @@ export function StatusStepper({
                 stageLabel={label}
                 waitingLabel={waitingLabel}
                 moreLabel={t('modern.moreStatuses')}
+                addLabel={canAddStatus ? t('modern.addStatus') : null}
                 under={under}
                 chosenId={sub?.id ?? null}
                 isCurrent={isCurrent}
@@ -171,6 +179,7 @@ function StageMenu({
   stageLabel,
   waitingLabel,
   moreLabel,
+  addLabel,
   under,
   chosenId,
   isCurrent,
@@ -180,6 +189,8 @@ function StageMenu({
   stageLabel: string
   waitingLabel: string
   moreLabel: string
+  /** The words for the way to add a status, or null for somebody who may not. */
+  addLabel: string | null
   under: WorkOrderStatusOption[]
   chosenId: string | null
   isCurrent: boolean
@@ -233,6 +244,23 @@ function StageMenu({
             {tick(chosenId === option.id)}
           </DropdownMenuItem>
         ))}
+        {addLabel && (
+          <>
+            <DropdownMenuSeparator />
+            {/* A new tab, like the page's other ways into settings: the job
+                may hold changes nobody has saved yet. */}
+            <DropdownMenuItem asChild className={cn(row, 'text-muted-foreground')}>
+              <Link
+                href="/settings/work-order-statuses"
+                target="_blank"
+                data-testid={`status-add-${stage}`}
+              >
+                <Plus className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
+                {addLabel}
+              </Link>
+            </DropdownMenuItem>
+          </>
+        )}
       </DropdownMenuContent>
     </DropdownMenu>
   )

+ 2 - 0
src/features/vehicles/Components/service-page/service-page-types.ts

@@ -125,6 +125,8 @@ export interface ServicePageClientProps {
   onlinePayments?: boolean
   /** The workshop's own statuses, for the menus on the status stepper. */
   workOrderStatuses?: WorkOrderStatusOption[]
+  /** The viewer may change settings, so the status menus offer to add a status. */
+  canAddStatus?: boolean
   /** The work order's video call, and the connected services that could add one. */
   videoCall?: ServiceVideoCall
   /** The workshop's tread limits, so a set checked in here grades correctly. */