Răsfoiți Sursa

add docs link (#254)

Bernt Christian Egeland 1 lună în urmă
părinte
comite
9dab433b5d

+ 42 - 0
src/components/docs-link.tsx

@@ -0,0 +1,42 @@
+'use client'
+
+import { useTranslations } from 'next-intl'
+import { BookOpen } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+/**
+ * Link from a page, card or dialog to the matching documentation page.
+ *
+ * `href` is a docs path without a locale (`/docs/features/inventory`). The docs
+ * site resolves the reader's own language, so a locale here would pin them to
+ * ours. Never append a heading anchor either: heading slugs are translated, so
+ * an English one does not exist on a localized page.
+ */
+export function DocsLink({
+  href,
+  className,
+  variant = 'inline',
+}: {
+  href: string
+  className?: string
+  /** `inline` sits beside a heading; `hint` sits under dialog text with an icon. */
+  variant?: 'inline' | 'hint'
+}) {
+  const t = useTranslations('navigation')
+
+  return (
+    <a
+      href={`https://torqvoice.com${href}`}
+      target="_blank"
+      rel="noopener noreferrer"
+      className={cn(
+        'shrink-0 rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
+        variant === 'hint' ? 'inline-flex items-center gap-1.5 text-xs' : 'text-[11px]',
+        className
+      )}
+    >
+      {variant === 'hint' && <BookOpen className="h-3.5 w-3.5" />}
+      {t('readMore')} →
+    </a>
+  )
+}

+ 20 - 10
src/components/page-header.tsx

@@ -19,6 +19,7 @@ import { AlertTriangle, Search, X, Zap } from 'lucide-react'
 import { useShowWhiteLabelCta } from '@/components/white-label-cta-context'
 import { useShowWhiteLabelCta } from '@/components/white-label-cta-context'
 import { useLicenseExpiry } from '@/components/license-expiry-context'
 import { useLicenseExpiry } from '@/components/license-expiry-context'
 import { QuickCreateMenu } from '@/components/quick-create-menu'
 import { QuickCreateMenu } from '@/components/quick-create-menu'
+import { DocsLink } from '@/components/docs-link'
 
 
 function SearchTrigger() {
 function SearchTrigger() {
   const t = useTranslations('navigation')
   const t = useTranslations('navigation')
@@ -73,6 +74,24 @@ const docsMap: Record<string, string> = {
   '/settings/webhooks': '/docs/integrations/webhooks',
   '/settings/webhooks': '/docs/integrations/webhooks',
   '/settings/subscription': '/docs/configuration/subscription',
   '/settings/subscription': '/docs/configuration/subscription',
   '/settings/license': '/docs/licensing/white-label',
   '/settings/license': '/docs/licensing/white-label',
+  '/': '/docs/features/dashboard',
+  '/settings/company': '/docs/configuration/workshop-profile',
+  '/settings/workshop': '/docs/configuration/workshop-profile',
+  '/settings/localization': '/docs/configuration/localization',
+  '/settings/appearance': '/docs/configuration/localization',
+  '/settings/tax': '/docs/configuration/tax',
+  '/settings/invoice': '/docs/configuration/invoice-numbering',
+  '/settings/payment': '/docs/configuration/payment-providers',
+  '/settings/custom-fields': '/docs/configuration/custom-fields',
+  '/settings/maintenance': '/docs/configuration/predicted-maintenance',
+  '/settings/data': '/docs/configuration/backup-and-restore',
+  '/settings/alerts': '/docs/features/low-stock-alerts',
+  '/settings/ai': '/docs/integrations/ai',
+  '/settings/telegram': '/docs/integrations/telegram',
+  '/telegram': '/docs/integrations/telegram',
+  '/settings/report-schedule': '/docs/features/reports',
+  '/settings/account': '/docs/security/passkeys-and-2fa',
+  '/sales': '/docs/features/work-orders',
 }
 }
 
 
 const breadcrumbMap: Record<string, BreadcrumbSegment[]> = {
 const breadcrumbMap: Record<string, BreadcrumbSegment[]> = {
@@ -198,16 +217,7 @@ export function PageHeader() {
         </BreadcrumbList>
         </BreadcrumbList>
       </Breadcrumb>
       </Breadcrumb>
       <div className="ml-auto flex items-center gap-2">
       <div className="ml-auto flex items-center gap-2">
-        {docsHref && (
-          <a
-            href={`https://torqvoice.com${docsHref}`}
-            target="_blank"
-            rel="noopener noreferrer"
-            className="hidden shrink-0 rounded text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:inline"
-          >
-            {tn('readMore')} →
-          </a>
-        )}
+        {docsHref && <DocsLink href={docsHref} className="hidden sm:inline" />}
         <SearchTrigger />
         <SearchTrigger />
         <QuickCreateMenu />
         <QuickCreateMenu />
         {showWhiteLabelCta && (
         {showWhiteLabelCta && (

+ 2 - 0
src/features/custom-fields/Components/CustomFieldsManager.tsx

@@ -18,6 +18,7 @@ import {
   SelectValue,
   SelectValue,
 } from '@/components/ui/select'
 } from '@/components/ui/select'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { DocsLink } from '@/components/docs-link'
 import { useGlassModal } from '@/components/glass-modal'
 import { useGlassModal } from '@/components/glass-modal'
 import { useConfirm } from '@/components/confirm-dialog'
 import { useConfirm } from '@/components/confirm-dialog'
 import {
 import {
@@ -302,6 +303,7 @@ export function CustomFieldsManager({
             <DialogTitle>
             <DialogTitle>
               {editing ? t('customFields.editField') : t('customFields.newField')}
               {editing ? t('customFields.editField') : t('customFields.newField')}
             </DialogTitle>
             </DialogTitle>
+            <DocsLink href="/docs/configuration/custom-fields" variant="hint" className="self-start" />
           </DialogHeader>
           </DialogHeader>
           <div className="space-y-4">
           <div className="space-y-4">
             <div className="space-y-2">
             <div className="space-y-2">

+ 2 - 0
src/features/customers/Components/CustomerForm.tsx

@@ -15,6 +15,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { useGlassModal } from "@/components/glass-modal";
 import { useGlassModal } from "@/components/glass-modal";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { createCustomer, updateCustomer } from "../Actions/customerActions";
 import { createCustomer, updateCustomer } from "../Actions/customerActions";
@@ -89,6 +90,7 @@ export function CustomerForm({ open, onOpenChange, customer, onCreated }: Custom
           <DialogTitle>
           <DialogTitle>
             {customer ? t("editTitle") : t("addTitle")}
             {customer ? t("editTitle") : t("addTitle")}
           </DialogTitle>
           </DialogTitle>
+          <DocsLink href="/docs/features/customers" variant="hint" className="self-start" />
           <DialogDescription className="sr-only">
           <DialogDescription className="sr-only">
             {customer ? t("editTitle") : t("addTitle")}
             {customer ? t("editTitle") : t("addTitle")}
           </DialogDescription>
           </DialogDescription>

+ 2 - 0
src/features/customers/Components/ImportCustomersDialog.tsx

@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
 import {
 import {
   Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
   Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import {
 import {
   Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
   Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
 } from "@/components/ui/table";
 } from "@/components/ui/table";
@@ -313,6 +314,7 @@ export function ImportCustomersDialog({
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t("title")}</DialogTitle>
           <DialogTitle>{t("title")}</DialogTitle>
           <DialogDescription>{t("description")}</DialogDescription>
           <DialogDescription>{t("description")}</DialogDescription>
+          <DocsLink href="/docs/tutorials/migrate-customers" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
 
 
         {step === "upload" && (
         {step === "upload" && (

+ 2 - 0
src/features/dashboard/Components/CustomCardDialog.tsx

@@ -13,6 +13,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import {
 import {
   Select,
   Select,
   SelectContent,
   SelectContent,
@@ -188,6 +189,7 @@ export function CustomCardDialog({
       <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl" aria-describedby={undefined}>
       <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl" aria-describedby={undefined}>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{widget ? t("editCard") : t("addCard")}</DialogTitle>
           <DialogTitle>{widget ? t("editCard") : t("addCard")}</DialogTitle>
+          <DocsLink href="/docs/features/dashboard" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
 
 
         <div className="space-y-5">
         <div className="space-y-5">

+ 2 - 0
src/features/inspections/Components/NewInspectionDialog.tsx

@@ -4,6 +4,7 @@ import { useState, useEffect, useTransition } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { DocsLink } from '@/components/docs-link'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
@@ -121,6 +122,7 @@ export function NewInspectionDialog({
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t("title")}</DialogTitle>
           <DialogTitle>{t("title")}</DialogTitle>
+          <DocsLink href="/docs/features/inspections" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
         <form onSubmit={handleSubmit} className="space-y-4">
         <form onSubmit={handleSubmit} className="space-y-4">
           <div className="space-y-2">
           <div className="space-y-2">

+ 2 - 0
src/features/inventory/Components/InventoryPartForm.tsx

@@ -16,6 +16,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { useGlassModal } from "@/components/glass-modal";
 import { useGlassModal } from "@/components/glass-modal";
 import { createInventoryPart, updateInventoryPart, deleteOrphanedUploads } from "../Actions/inventoryActions";
 import { createInventoryPart, updateInventoryPart, deleteOrphanedUploads } from "../Actions/inventoryActions";
@@ -338,6 +339,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
             <DialogTitle>
             <DialogTitle>
               {part ? t('form.editPart') : t('form.addNewPart')}
               {part ? t('form.editPart') : t('form.addNewPart')}
             </DialogTitle>
             </DialogTitle>
+            <DocsLink href="/docs/features/inventory" className="ml-auto" />
             {/* Only meaningful for a saved part — a new one has no history yet.
             {/* Only meaningful for a saved part — a new one has no history yet.
                 Opens on top of this dialog rather than replacing it, so
                 Opens on top of this dialog rather than replacing it, so
                 in-progress edits and uploads are not discarded. */}
                 in-progress edits and uploads are not discarded. */}

+ 2 - 0
src/features/labor-presets/Components/LaborPresetForm.tsx

@@ -13,6 +13,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { useGlassModal } from "@/components/glass-modal";
 import { useGlassModal } from "@/components/glass-modal";
 import { createLaborPreset, updateLaborPreset } from "../Actions/laborPresetActions";
 import { createLaborPreset, updateLaborPreset } from "../Actions/laborPresetActions";
 import { Loader2, Plus, Trash2 } from "lucide-react";
 import { Loader2, Plus, Trash2 } from "lucide-react";
@@ -257,6 +258,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
           <DialogTitle>
           <DialogTitle>
             {preset ? t("form.editPackage") : t("form.addPackage")}
             {preset ? t("form.editPackage") : t("form.addPackage")}
           </DialogTitle>
           </DialogTitle>
+          <DocsLink href="/docs/configuration/work-orders/labor-presets" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
 
 
         {/* Mode tabs */}
         {/* Mode tabs */}

+ 2 - 0
src/features/quotes/Components/NewQuoteDialog.tsx

@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'
 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { DocsLink } from '@/components/docs-link'
 import { Loader2 } from 'lucide-react'
 import { Loader2 } from 'lucide-react'
 import { createQuote } from '../Actions/quoteActions'
 import { createQuote } from '../Actions/quoteActions'
 import { VehicleCombobox } from './VehicleCombobox'
 import { VehicleCombobox } from './VehicleCombobox'
@@ -99,6 +100,7 @@ export function NewQuoteDialog({
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t('form.newQuote')}</DialogTitle>
           <DialogTitle>{t('form.newQuote')}</DialogTitle>
+          <DocsLink href="/docs/features/quotes" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
         <form onSubmit={handleCreate} className="space-y-4">
         <form onSubmit={handleCreate} className="space-y-4">
           <div className="space-y-2">
           <div className="space-y-2">

+ 2 - 0
src/features/scheduled-messages/Components/ScheduleMessageDialog.tsx

@@ -22,6 +22,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
 import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
 import {
 import {
   createScheduledMessage,
   createScheduledMessage,
@@ -184,6 +185,7 @@ export function ScheduleMessageDialog({
       <DialogContent className="sm:max-w-lg" aria-describedby={undefined}>
       <DialogContent className="sm:max-w-lg" aria-describedby={undefined}>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{isEdit ? t("editTitle") : t("title")}</DialogTitle>
           <DialogTitle>{isEdit ? t("editTitle") : t("title")}</DialogTitle>
+          <DocsLink href="/docs/features/messages" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
 
 
         <form onSubmit={handleSubmit} className="space-y-4">
         <form onSubmit={handleSubmit} className="space-y-4">

+ 2 - 0
src/features/sms/Components/NewSmsDialog.tsx

@@ -14,6 +14,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { useDebouncedSearch } from "@/hooks/use-debounced-search";
 import { useDebouncedSearch } from "@/hooks/use-debounced-search";
 import { searchSmsRecipients, sendSmsToCustomer } from "../Actions/smsActions";
 import { searchSmsRecipients, sendSmsToCustomer } from "../Actions/smsActions";
 import { SmsTemplateMenu } from "./SmsTemplateMenu";
 import { SmsTemplateMenu } from "./SmsTemplateMenu";
@@ -108,6 +109,7 @@ export function NewSmsDialog({
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
       <DialogContent className="sm:max-w-md" aria-describedby={undefined}>
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t("title")}</DialogTitle>
           <DialogTitle>{t("title")}</DialogTitle>
+          <DocsLink href="/docs/integrations/sms" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
 
 
         {!recipient ? (
         {!recipient ? (

+ 2 - 0
src/features/status-reports/Components/CreateStatusReportDialog.tsx

@@ -6,6 +6,7 @@ import {
   Dialog, DialogContent, DialogHeader, DialogTitle,
   Dialog, DialogContent, DialogHeader, DialogTitle,
   DialogDescription, DialogFooter,
   DialogDescription, DialogFooter,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import {
 import {
   Drawer, DrawerContent, DrawerHeader, DrawerTitle,
   Drawer, DrawerContent, DrawerHeader, DrawerTitle,
   DrawerDescription, DrawerFooter,
   DrawerDescription, DrawerFooter,
@@ -232,6 +233,7 @@ export function CreateStatusReportDialog({
       <DialogContent className="sm:max-w-lg">
       <DialogContent className="sm:max-w-lg">
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t("title")}</DialogTitle>
           <DialogTitle>{t("title")}</DialogTitle>
+          <DocsLink href="/docs/features/status-reports" variant="hint" className="self-start" />
           <DialogDescription>{vehicleName}</DialogDescription>
           <DialogDescription>{vehicleName}</DialogDescription>
         </DialogHeader>
         </DialogHeader>
         {formContent}
         {formContent}

+ 2 - 0
src/features/vehicles/Components/ReminderFormDialog.tsx

@@ -25,6 +25,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { CalendarIcon, Check, ChevronsUpDown, Loader2 } from "lucide-react";
 import { CalendarIcon, Check, ChevronsUpDown, Loader2 } from "lucide-react";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
@@ -164,6 +165,7 @@ export function ReminderFormDialog({
       <DialogContent className="sm:max-w-md">
       <DialogContent className="sm:max-w-md">
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{isEdit ? tv("editTitle") : tv("addTitle")}</DialogTitle>
           <DialogTitle>{isEdit ? tv("editTitle") : tv("addTitle")}</DialogTitle>
+          <DocsLink href="/docs/features/reminders" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
         <form onSubmit={handleSubmit} className="space-y-4">
         <form onSubmit={handleSubmit} className="space-y-4">
           {/* Target: workshop (nothing), customer, or vehicle */}
           {/* Target: workshop (nothing), customer, or vehicle */}

+ 2 - 0
src/features/vehicles/Components/VehicleForm.tsx

@@ -34,6 +34,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { useGlassModal } from "@/components/glass-modal";
 import { useGlassModal } from "@/components/glass-modal";
 import { createVehicle, updateVehicle } from "../Actions/vehicleActions";
 import { createVehicle, updateVehicle } from "../Actions/vehicleActions";
@@ -196,6 +197,7 @@ export function VehicleForm({ open, onOpenChange, vehicle, customers, defaultCus
           <DialogTitle>
           <DialogTitle>
             {vehicle ? t("editTitle") : t("addTitle")}
             {vehicle ? t("editTitle") : t("addTitle")}
           </DialogTitle>
           </DialogTitle>
+          <DocsLink href="/docs/features/vehicles" variant="hint" className="self-start" />
           <DialogDescription className="sr-only">
           <DialogDescription className="sr-only">
             {vehicle ? t("editTitle") : t("addTitle")}
             {vehicle ? t("editTitle") : t("addTitle")}
           </DialogDescription>
           </DialogDescription>

+ 2 - 0
src/features/workboard/Components/TechnicianDialog.tsx

@@ -7,6 +7,7 @@ import {
   DialogHeader,
   DialogHeader,
   DialogTitle,
   DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
+import { DocsLink } from "@/components/docs-link";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
@@ -122,6 +123,7 @@ export function TechnicianDialog({
           <DialogTitle>
           <DialogTitle>
             {technician ? t("editTitle") : t("addTitle")}
             {technician ? t("editTitle") : t("addTitle")}
           </DialogTitle>
           </DialogTitle>
+          <DocsLink href="/docs/configuration/work-orders/technician-assignment" variant="hint" className="self-start" />
         </DialogHeader>
         </DialogHeader>
         <form onSubmit={handleSubmit} className="space-y-4">
         <form onSubmit={handleSubmit} className="space-y-4">
           <div className="space-y-2">
           <div className="space-y-2">