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

Split the templates menu into Documents, Checklists and Messages (#429)

Bernt Christian Egeland 4 дней назад
Родитель
Сommit
fbea6be0a9

+ 8 - 5
messages/de/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Rechnung",
       "quotation": "Angebot",
-      "inspections": "Inspektions-\nChecklisten",
-      "sms": "SMS",
-      "layout": "Layout",
-      "certificates": "Inspektions-\nZertifikate",
-      "workOrders": "Arbeits-\naufträge"
+      "workOrders": "Arbeitsauftrag",
+      "certificates": "Inspektionszertifikat",
+      "layout": "Layout"
+    },
+    "sections": {
+      "documents": "Dokumente",
+      "checklists": "Checklisten",
+      "messages": "Nachrichten"
     },
     "presets": "Vorlagen-Presets",
     "primaryColor": "Primärfarbe",

+ 8 - 5
messages/en/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Invoice",
       "quotation": "Quotation",
-      "inspections": "Inspection\nChecklists",
-      "sms": "SMS",
-      "layout": "Layout",
-      "certificates": "Inspection\nCertificates",
-      "workOrders": "Work\nOrders"
+      "workOrders": "Work order",
+      "certificates": "Inspection certificate",
+      "layout": "Layout"
+    },
+    "sections": {
+      "documents": "Documents",
+      "checklists": "Checklists",
+      "messages": "Messages"
     },
     "presets": "Template Presets",
     "primaryColor": "Primary Color",

+ 8 - 5
messages/es/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Factura",
       "quotation": "Cotización",
-      "inspections": "Listas de\ninspección",
-      "sms": "SMS",
-      "layout": "Disposición",
-      "certificates": "Certificados de\ninspección",
-      "workOrders": "Órdenes\nde trabajo"
+      "workOrders": "Orden de trabajo",
+      "certificates": "Certificado de inspección",
+      "layout": "Disposición"
+    },
+    "sections": {
+      "documents": "Documentos",
+      "checklists": "Listas de control",
+      "messages": "Mensajes"
     },
     "presets": "Presets de plantilla",
     "primaryColor": "Color primario",

+ 8 - 5
messages/fr/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Facture",
       "quotation": "Devis",
-      "inspections": "Listes\nd'inspection",
-      "sms": "SMS",
-      "layout": "Mise en page",
-      "certificates": "Certificats\nd'inspection",
-      "workOrders": "Ordres\nde travail"
+      "workOrders": "Ordre de travail",
+      "certificates": "Certificat d'inspection",
+      "layout": "Mise en page"
+    },
+    "sections": {
+      "documents": "Documents",
+      "checklists": "Listes de contrôle",
+      "messages": "Messages"
     },
     "presets": "Presets de modèle",
     "primaryColor": "Couleur primaire",

+ 8 - 5
messages/it/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Fattura",
       "quotation": "Preventivo",
-      "inspections": "Checklist\nispezione",
-      "sms": "SMS",
-      "layout": "Layout",
-      "certificates": "Certificati\nispezione",
-      "workOrders": "Ordini\ndi lavoro"
+      "workOrders": "Ordine di lavoro",
+      "certificates": "Certificato di ispezione",
+      "layout": "Layout"
+    },
+    "sections": {
+      "documents": "Documenti",
+      "checklists": "Checklist",
+      "messages": "Messaggi"
     },
     "presets": "Preset modello",
     "primaryColor": "Colore primario",

+ 8 - 5
messages/lt/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Sąskaita",
       "quotation": "Pasiūlymas",
-      "inspections": "Apžiūros\nkontroliniai sąrašai",
-      "sms": "SMS",
-      "layout": "Išdėstymas",
-      "certificates": "Apžiūros\nsertifikatai",
-      "workOrders": "Darbo\nužsakymai"
+      "workOrders": "Darbo užsakymas",
+      "certificates": "Apžiūros sertifikatas",
+      "layout": "Išdėstymas"
+    },
+    "sections": {
+      "documents": "Dokumentai",
+      "checklists": "Kontroliniai sąrašai",
+      "messages": "Žinutės"
     },
     "presets": "Šablonų presetai",
     "primaryColor": "Pagrindinė spalva",

+ 8 - 5
messages/nb/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Faktura",
       "quotation": "Tilbud",
-      "inspections": "Inspeksjons-\nsjekklister",
-      "sms": "SMS",
-      "layout": "Oppsett",
-      "certificates": "Inspeksjons-\nsertifikater",
-      "workOrders": "Arbeids-\nordrer"
+      "workOrders": "Arbeidsordre",
+      "certificates": "Inspeksjonssertifikat",
+      "layout": "Oppsett"
+    },
+    "sections": {
+      "documents": "Dokumenter",
+      "checklists": "Sjekklister",
+      "messages": "Meldinger"
     },
     "presets": "Malforhåndsvisninger",
     "primaryColor": "Primærfarge",

+ 8 - 5
messages/nl/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Factuur",
       "quotation": "Offerte",
-      "inspections": "Inspectie-\nchecklists",
-      "sms": "SMS",
-      "layout": "Indeling",
-      "certificates": "Inspectie-\ncertificaten",
-      "workOrders": "Werk-\norders"
+      "workOrders": "Werkorder",
+      "certificates": "Inspectiecertificaat",
+      "layout": "Indeling"
+    },
+    "sections": {
+      "documents": "Documenten",
+      "checklists": "Checklists",
+      "messages": "Berichten"
     },
     "presets": "Sjabloonpresets",
     "primaryColor": "Primaire kleur",

+ 8 - 5
messages/pl/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Faktura",
       "quotation": "Wycena",
-      "inspections": "Listy kontrolne\nprzeglądów",
-      "sms": "SMS",
-      "layout": "Układ",
-      "certificates": "Certyfikaty\nprzeglądów",
-      "workOrders": "Zlecenia"
+      "workOrders": "Zlecenie",
+      "certificates": "Certyfikat przeglądu",
+      "layout": "Układ"
+    },
+    "sections": {
+      "documents": "Dokumenty",
+      "checklists": "Listy kontrolne",
+      "messages": "Wiadomości"
     },
     "presets": "Gotowe szablony",
     "primaryColor": "Kolor główny",

+ 8 - 5
messages/pt-BR/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Fatura",
       "quotation": "Orçamento",
-      "inspections": "Checklists de\ninspeção",
-      "sms": "SMS",
-      "layout": "Layout",
-      "certificates": "Certificados de\ninspeção",
-      "workOrders": "Ordens\nde serviço"
+      "workOrders": "Ordem de serviço",
+      "certificates": "Certificado de inspeção",
+      "layout": "Layout"
+    },
+    "sections": {
+      "documents": "Documentos",
+      "checklists": "Checklists",
+      "messages": "Mensagens"
     },
     "presets": "Presets de modelo",
     "primaryColor": "Cor primária",

+ 8 - 5
messages/pt-PT/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Fatura",
       "quotation": "Orçamento",
-      "inspections": "Listas de\ninspeção",
-      "sms": "SMS",
-      "layout": "Layout",
-      "certificates": "Certificados de\ninspeção",
-      "workOrders": "Ordens\nde trabalho"
+      "workOrders": "Ordem de trabalho",
+      "certificates": "Certificado de inspeção",
+      "layout": "Layout"
+    },
+    "sections": {
+      "documents": "Documentos",
+      "checklists": "Listas de verificação",
+      "messages": "Mensagens"
     },
     "presets": "Modelos predefinidos",
     "primaryColor": "Cor primária",

+ 8 - 5
messages/ru/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Счёт",
       "quotation": "Предложение",
-      "inspections": "Чек-листы\nосмотров",
-      "sms": "SMS",
-      "layout": "Макет",
-      "certificates": "Сертификаты\nосмотров",
-      "workOrders": "Заказ-\nнаряды"
+      "workOrders": "Заказ-наряд",
+      "certificates": "Сертификат осмотра",
+      "layout": "Макет"
+    },
+    "sections": {
+      "documents": "Документы",
+      "checklists": "Чек-листы",
+      "messages": "Сообщения"
     },
     "presets": "Предустановки шаблонов",
     "primaryColor": "Основной цвет",

+ 8 - 5
messages/tr/settings.json

@@ -857,11 +857,14 @@
     "tabs": {
       "invoice": "Fatura",
       "quotation": "Teklif",
-      "inspections": "Muayene\nkontrol listeleri",
-      "sms": "SMS",
-      "layout": "Düzen",
-      "certificates": "Muayene\nsertifikaları",
-      "workOrders": "İş\nemirleri"
+      "workOrders": "İş emri",
+      "certificates": "Muayene sertifikası",
+      "layout": "Düzen"
+    },
+    "sections": {
+      "documents": "Belgeler",
+      "checklists": "Kontrol listeleri",
+      "messages": "Mesajlar"
     },
     "presets": "Şablon Hazır Ayarları",
     "primaryColor": "Ana Renk",

+ 103 - 121
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -14,7 +14,16 @@ import { toast } from 'sonner'
 import { setSetting } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { presetsFor } from '@/features/settings/Schema/layoutPresets'
-import { Check, Loader2, Palette, MessageSquare, RotateCcw } from 'lucide-react'
+import {
+  Check,
+  FileText,
+  ListChecks,
+  Loader2,
+  type LucideIcon,
+  MessageSquare,
+  Palette,
+  RotateCcw,
+} from 'lucide-react'
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from '../read-only-guard'
 import { cn } from '@/lib/utils'
 import { TemplateListClient } from '@/features/inspections/Components/TemplateListClient'
@@ -41,6 +50,17 @@ interface TemplateValues {
 
 type TabType = 'invoice' | 'quotation' | 'workOrders' | 'certificates' | 'inspections' | 'sms'
 
+type SectionType = 'documents' | 'checklists' | 'messages'
+
+/** The printed documents, in the order their chips run. */
+const DOCUMENT_TABS = ['invoice', 'quotation', 'workOrders', 'certificates'] as const
+
+function sectionOf(tab: TabType): SectionType {
+  if (tab === 'inspections') return 'checklists'
+  if (tab === 'sms') return 'messages'
+  return 'documents'
+}
+
 interface WorkshopPreviewInfo {
   name?: string
   address?: string
@@ -139,23 +159,6 @@ function ColorRow({
  * with the sheet in front of you, rather than split across a colour form here
  * and an arrangement editor two clicks away.
  */
-/**
- * A tab's name, on two lines when the translation carries a line break: the
- * first line at the tab's size, the second smaller and quieter, so
- * "Inspection / Checklists" and "Inspection / Certificates" read as one
- * family without widening the row.
- */
-function TabLabel({ text }: { text: string }) {
-  const [first, ...rest] = text.split('\n')
-  if (rest.length === 0) return <>{text}</>
-  return (
-    <span className="flex flex-col items-center leading-tight">
-      <span>{first}</span>
-      <span className="text-[11px] font-normal opacity-75">{rest.join(' ')}</span>
-    </span>
-  )
-}
-
 function TemplateTab({
   documentType,
   workshop,
@@ -569,6 +572,15 @@ export function TemplateSettings({
     },
     [router, searchParams]
   )
+  const section = sectionOf(tab)
+  const sections: { id: SectionType; tab: TabType; icon: LucideIcon }[] = [
+    { id: 'documents', tab: 'invoice', icon: FileText },
+    { id: 'checklists', tab: 'inspections', icon: ListChecks },
+    // Shown when a link lands on it too, so an old ?tab=sms still opens.
+    ...(smsEnabled || tab === 'sms'
+      ? [{ id: 'messages' as const, tab: 'sms' as const, icon: MessageSquare }]
+      : []),
+  ]
   const [saving, setSaving] = useState(false)
   const [invoiceValues, setInvoiceValues] = useState(initialInvoiceValues)
   const [quoteValues, setQuoteValues] = useState(initialQuoteValues)
@@ -620,112 +632,82 @@ export function TemplateSettings({
   return (
     <div className="space-y-6">
       <ReadOnlyBanner />
-      <div>
-        <div className="flex flex-wrap items-baseline justify-between gap-2">
-          <h2 className="text-lg font-semibold">{t('templates.title')}</h2>
-          {/* Colors live here and arrangement lives there, which is easy to
-              get lost in. Each page says where the other half is. */}
-          {tab !== 'inspections' &&
-            tab !== 'sms' &&
-            tab !== 'certificates' &&
-            tab !== 'workOrders' && (
-              <Link
-                href="/settings/invoice?tab=layout"
-                className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
-              >
-                {t('templates.goToLayout')}
-              </Link>
-            )}
-        </div>
-        <p className="text-sm text-muted-foreground">
-          {tab === 'inspections'
-            ? t('templates.inspectionsDescription')
-            : tab === 'certificates'
-              ? t('templates.certificatesDescription')
-              : tab === 'workOrders'
-                ? t('templates.workOrdersDescription')
-                : tab === 'sms'
-                  ? t('templates.smsDescription')
-                  : t('templates.invoiceDescription')}
-        </p>
-      </div>
+      <h2 className="text-lg font-semibold">{t('templates.title')}</h2>
 
-      {/* Tab Buttons */}
-      <div className="flex gap-1 rounded-lg border bg-muted p-1">
-        <button
-          type="button"
-          onClick={() => setTab('invoice')}
-          className={cn(
-            'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-            tab === 'invoice'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
-        >
-          <TabLabel text={t('templates.tabs.invoice')} />
-        </button>
-        <button
-          type="button"
-          onClick={() => setTab('quotation')}
-          className={cn(
-            'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-            tab === 'quotation'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
-        >
-          <TabLabel text={t('templates.tabs.quotation')} />
-        </button>
-        <button
-          type="button"
-          onClick={() => setTab('workOrders')}
-          className={cn(
-            'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-            tab === 'workOrders'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
-        >
-          <TabLabel text={t('templates.tabs.workOrders')} />
-        </button>
-        <button
-          type="button"
-          onClick={() => setTab('certificates')}
-          className={cn(
-            'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-            tab === 'certificates'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
-        >
-          <TabLabel text={t('templates.tabs.certificates')} />
-        </button>
-        <button
-          type="button"
-          onClick={() => setTab('inspections')}
-          className={cn(
-            'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-            tab === 'inspections'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
+      {/* Two levels: what kind of template, then which document. The page
+          holds three different editors, and the four printed documents are
+          only one of them. */}
+      <div className="space-y-3">
+        <div
+          role="tablist"
+          aria-label={t('templates.title')}
+          className="flex w-full gap-1 overflow-x-auto rounded-lg border bg-muted p-1 sm:w-fit"
         >
-          <TabLabel text={t('templates.tabs.inspections')} />
-        </button>
-        {smsEnabled && (
-          <button
-            type="button"
-            onClick={() => setTab('sms')}
-            className={cn(
-              'flex flex-1 items-center justify-center rounded-md px-4 py-2 text-center text-sm font-medium transition-colors',
-              tab === 'sms'
-                ? 'bg-background text-foreground shadow-sm'
-                : 'text-muted-foreground hover:text-foreground'
-            )}
-          >
-            <TabLabel text={t('templates.tabs.sms')} />
-          </button>
+          {sections.map(({ id, tab: target, icon: Icon }) => (
+            <button
+              key={id}
+              type="button"
+              role="tab"
+              aria-selected={section === id}
+              onClick={() => setTab(target)}
+              className={cn(
+                'flex flex-1 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors sm:flex-none sm:px-4',
+                section === id
+                  ? 'bg-background text-foreground shadow-sm'
+                  : 'text-muted-foreground hover:text-foreground'
+              )}
+            >
+              <Icon className={cn('h-4 w-4', section === id && 'text-primary')} />
+              {t(`templates.sections.${id}`)}
+            </button>
+          ))}
+        </div>
+
+        {section === 'documents' && (
+          <div role="tablist" className="flex gap-2 overflow-x-auto">
+            {DOCUMENT_TABS.map((doc) => (
+              <button
+                key={doc}
+                type="button"
+                role="tab"
+                aria-selected={tab === doc}
+                onClick={() => setTab(doc)}
+                className={cn(
+                  'shrink-0 whitespace-nowrap rounded-full border px-3 py-1 text-sm font-medium transition-colors',
+                  tab === doc
+                    ? 'border-foreground bg-foreground text-background'
+                    : 'bg-card text-muted-foreground hover:text-foreground'
+                )}
+              >
+                {t(`templates.tabs.${doc}`)}
+              </button>
+            ))}
+          </div>
         )}
+
+        <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
+          <p className="text-sm text-muted-foreground">
+            {tab === 'inspections'
+              ? t('templates.inspectionsDescription')
+              : tab === 'certificates'
+                ? t('templates.certificatesDescription')
+                : tab === 'workOrders'
+                  ? t('templates.workOrdersDescription')
+                  : tab === 'sms'
+                    ? t('templates.smsDescription')
+                    : t('templates.invoiceDescription')}
+          </p>
+          {/* Colors live here and arrangement lives there, which is easy to
+              get lost in. Each page says where the other half is. */}
+          {(tab === 'invoice' || tab === 'quotation') && (
+            <Link
+              href="/settings/invoice?tab=layout"
+              className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
+            >
+              {t('templates.goToLayout')}
+            </Link>
+          )}
+        </div>
       </div>
 
       {tab === 'inspections' ? (