Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
313420396c

+ 0 - 293
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -1,20 +1,12 @@
 'use client'
 
 import { useRef, useState } from 'react'
-import Image from 'next/image'
 import { Button } from '@/components/ui/button'
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Checkbox } from '@/components/ui/checkbox'
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-} from '@/components/ui/dialog'
 import { useGlassModal } from '@/components/glass-modal'
 import {
   AlertTriangle,
-  ArrowRight,
   Download,
   FileArchive,
   Loader2,
@@ -153,84 +145,6 @@ export function DataSettings() {
     setImporting(false)
   }
 
-  // ── LubeLog import dialog ──────────────────────────────────────────────
-  const lubelogInputRef = useRef<HTMLInputElement>(null)
-  const [lubelogOpen, setLubelogOpen] = useState(false)
-  const [lubelogFile, setLubelogFile] = useState<File | null>(null)
-  const [importingLubelog, setImportingLubelog] = useState(false)
-
-  const handleLubeLogImport = async () => {
-    if (!lubelogFile) return
-
-    setImportingLubelog(true)
-    try {
-      const buffer = await lubelogFile.arrayBuffer()
-      const res = await fetch('/api/backup/import-lubelog', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/zip' },
-        body: buffer,
-      })
-
-      if (!res.ok) {
-        const body = await res.json()
-        throw new Error(body.error || 'Import failed')
-      }
-
-      const result = await res.json()
-      setLubelogFile(null)
-      if (lubelogInputRef.current) lubelogInputRef.current.value = ''
-      setLubelogOpen(false)
-      modal.open(
-        'success',
-        'LubeLog Import Complete',
-        `Imported ${result.imported.vehicles} vehicles, ${result.imported.serviceRecords} service records, and ${result.imported.notes} notes.`
-      )
-    } catch (err) {
-      const message = err instanceof Error ? err.message : 'Import failed'
-      modal.open('error', 'Import Failed', message)
-    }
-    setImportingLubelog(false)
-  }
-
-  // ── Invoice Ninja import dialog ──────────────────────────────────────────
-  const invoiceNinjaInputRef = useRef<HTMLInputElement>(null)
-  const [invoiceNinjaOpen, setInvoiceNinjaOpen] = useState(false)
-  const [invoiceNinjaFile, setInvoiceNinjaFile] = useState<File | null>(null)
-  const [importingInvoiceNinja, setImportingInvoiceNinja] = useState(false)
-
-  const handleInvoiceNinjaImport = async () => {
-    if (!invoiceNinjaFile) return
-
-    setImportingInvoiceNinja(true)
-    try {
-      const buffer = await invoiceNinjaFile.arrayBuffer()
-      const res = await fetch('/api/backup/import-invoice-ninja', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/zip' },
-        body: buffer,
-      })
-
-      if (!res.ok) {
-        const body = await res.json()
-        throw new Error(body.error || 'Import failed')
-      }
-
-      const result = await res.json()
-      setInvoiceNinjaFile(null)
-      if (invoiceNinjaInputRef.current) invoiceNinjaInputRef.current.value = ''
-      setInvoiceNinjaOpen(false)
-      modal.open(
-        'success',
-        'Invoice Ninja Import Complete',
-        `Imported ${result.imported.customers} customers, ${result.imported.products} products, ${result.imported.invoices} invoices, ${result.imported.documents} documents, and ${result.imported.payments} payments.`
-      )
-    } catch (err) {
-      const message = err instanceof Error ? err.message : 'Import failed'
-      modal.open('error', 'Import Failed', message)
-    }
-    setImportingInvoiceNinja(false)
-  }
-
   return (
     <div className="space-y-6">
       <ReadOnlyBanner />
@@ -339,214 +253,7 @@ export function DataSettings() {
           </CardContent>
         </Card>
       </div>
-
-      {/* Import from Other Services */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <ArrowRight className="h-4 w-4" /> Import from Other Services
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
-          <p className="text-sm text-muted-foreground">
-            Migrate your data from another platform. Select a service to get started.
-          </p>
-          <div className="flex flex-wrap gap-4">
-            {/* LubeLog */}
-            <button
-              type="button"
-              onClick={() => setLubelogOpen(true)}
-              className="group flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-all hover:border-primary/50 hover:shadow-md"
-            >
-              <Image
-                src="/images/import/lubelog.png"
-                alt="LubeLog"
-                width={120}
-                height={30}
-                className="object-contain"
-                unoptimized
-              />
-            </button>
-
-            {/* Invoice Ninja */}
-            <button
-              type="button"
-              onClick={() => setInvoiceNinjaOpen(true)}
-              className="group flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-all hover:border-primary/50 hover:shadow-md"
-            >
-              <Image
-                src="/images/import/invoice_ninja.png"
-                alt="Invoice Ninja"
-                width={140}
-                height={30}
-                className="object-contain"
-                unoptimized
-              />
-            </button>
-          </div>
-        </CardContent>
-      </Card>
       </ReadOnlyWrapper>
-
-      {/* LubeLog Import Dialog */}
-      <Dialog open={lubelogOpen} onOpenChange={(open) => {
-        setLubelogOpen(open)
-        if (!open) {
-          setLubelogFile(null)
-          if (lubelogInputRef.current) lubelogInputRef.current.value = ''
-        }
-      }}>
-        <DialogContent className="sm:max-w-lg">
-          <DialogHeader>
-            <DialogTitle className="flex items-center gap-3">
-              <Image
-                src="/images/import/lubelog.png"
-                alt="LubeLog"
-                width={24}
-                height={24}
-                className="object-contain"
-                unoptimized
-              />
-              Import from LubeLog
-            </DialogTitle>
-          </DialogHeader>
-
-          <div className="space-y-4">
-            <div className="space-y-3">
-              <p className="text-sm font-medium">How to export your LubeLog data:</p>
-              <ol className="list-inside list-decimal space-y-1.5 text-sm text-muted-foreground">
-                <li>Open your LubeLog instance</li>
-                <li>Go to <strong className="text-foreground">Settings</strong></li>
-                <li>Click <strong className="text-foreground">Make Backup</strong> to download a zip file</li>
-                <li>Upload the downloaded zip file below</li>
-              </ol>
-            </div>
-
-            <div className="flex items-start gap-2 rounded-md bg-blue-500/10 p-3 text-sm text-blue-700 dark:text-blue-400">
-              <FileArchive className="mt-0.5 h-4 w-4 shrink-0" />
-              <span>
-                This will <strong>add data alongside</strong> your existing records — nothing will be deleted.
-              </span>
-            </div>
-
-            <div className="space-y-2">
-              <p className="text-sm font-medium">What will be imported:</p>
-              <ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
-                <li>Vehicles</li>
-                <li>Service records with attachments</li>
-                <li>Notes</li>
-              </ul>
-            </div>
-
-            <div className="space-y-3 border-t pt-4">
-              <input
-                ref={lubelogInputRef}
-                type="file"
-                accept=".zip"
-                onChange={(e) => setLubelogFile(e.target.files?.[0] ?? null)}
-                className="block w-full text-sm text-muted-foreground file:mr-4 file:rounded-md file:border-0 file:bg-primary/10 file:px-4 file:py-2 file:text-sm file:font-medium file:text-primary hover:file:bg-primary/20"
-              />
-              <p className="text-xs text-muted-foreground">
-                Files up to 200MB supported.
-              </p>
-            </div>
-
-            <div className="flex justify-end gap-2 pt-2">
-              <Button variant="ghost" onClick={() => setLubelogOpen(false)}>
-                Cancel
-              </Button>
-              <Button
-                onClick={handleLubeLogImport}
-                disabled={!lubelogFile || importingLubelog}
-              >
-                {importingLubelog && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-                Import
-              </Button>
-            </div>
-          </div>
-        </DialogContent>
-      </Dialog>
-
-      {/* Invoice Ninja Import Dialog */}
-      <Dialog open={invoiceNinjaOpen} onOpenChange={(open) => {
-        setInvoiceNinjaOpen(open)
-        if (!open) {
-          setInvoiceNinjaFile(null)
-          if (invoiceNinjaInputRef.current) invoiceNinjaInputRef.current.value = ''
-        }
-      }}>
-        <DialogContent className="sm:max-w-lg">
-          <DialogHeader>
-            <DialogTitle className="flex items-center gap-3">
-              <Image
-                src="/images/import/invoice_ninja.png"
-                alt="Invoice Ninja"
-                width={28}
-                height={28}
-                className="object-contain"
-                unoptimized
-              />
-              Import from Invoice Ninja
-            </DialogTitle>
-          </DialogHeader>
-
-          <div className="space-y-4">
-            <div className="space-y-3">
-              <p className="text-sm font-medium">How to export your Invoice Ninja data:</p>
-              <ol className="list-inside list-decimal space-y-1.5 text-sm text-muted-foreground">
-                <li>Open your Invoice Ninja instance</li>
-                <li>Go to <strong className="text-foreground">Settings &gt; Import | Export</strong></li>
-                <li>Click <strong className="text-foreground">Export</strong> to download a zip file</li>
-                <li>Upload the downloaded zip file below</li>
-              </ol>
-            </div>
-
-            <div className="flex items-start gap-2 rounded-md bg-blue-500/10 p-3 text-sm text-blue-700 dark:text-blue-400">
-              <FileArchive className="mt-0.5 h-4 w-4 shrink-0" />
-              <span>
-                This will <strong>add data alongside</strong> your existing records — nothing will be deleted.
-              </span>
-            </div>
-
-            <div className="space-y-2">
-              <p className="text-sm font-medium">What will be imported:</p>
-              <ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
-                <li>Customers (clients)</li>
-                <li>Products (inventory)</li>
-                <li>Invoices as service records (parts &amp; labor)</li>
-                <li>Documents &amp; attachments</li>
-                <li>Payments</li>
-              </ul>
-            </div>
-
-            <div className="space-y-3 border-t pt-4">
-              <input
-                ref={invoiceNinjaInputRef}
-                type="file"
-                accept=".zip"
-                onChange={(e) => setInvoiceNinjaFile(e.target.files?.[0] ?? null)}
-                className="block w-full text-sm text-muted-foreground file:mr-4 file:rounded-md file:border-0 file:bg-primary/10 file:px-4 file:py-2 file:text-sm file:font-medium file:text-primary hover:file:bg-primary/20"
-              />
-              <p className="text-xs text-muted-foreground">
-                Upload an Invoice Ninja zip export file. Files up to 200MB supported.
-              </p>
-            </div>
-
-            <div className="flex justify-end gap-2 pt-2">
-              <Button variant="ghost" onClick={() => setInvoiceNinjaOpen(false)}>
-                Cancel
-              </Button>
-              <Button
-                onClick={handleInvoiceNinjaImport}
-                disabled={!invoiceNinjaFile || importingInvoiceNinja}
-              >
-                {importingInvoiceNinja && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-                Import
-              </Button>
-            </div>
-          </div>
-        </DialogContent>
-      </Dialog>
     </div>
   )
 }

+ 215 - 41
src/app/(authenticated)/settings/invoice-template/template-settings.tsx

@@ -15,7 +15,8 @@ import {
 import { toast } from "sonner";
 import { setSetting } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
-import { Loader2, Palette } from "lucide-react";
+import { templatePresets } from "@/features/settings/Schema/templatePresets";
+import { Check, Loader2, Palette } from "lucide-react";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 interface TemplateValues {
@@ -41,6 +42,104 @@ const colorPresets = [
   { name: "Indigo", value: "#4f46e5" },
 ];
 
+function PreviewHeader({
+  values,
+  headerStyle,
+  fontFamily,
+}: {
+  values: TemplateValues;
+  headerStyle: string;
+  fontFamily: string;
+}) {
+  const style = { fontFamily: fontMap[fontFamily] || "sans-serif" };
+
+  if (headerStyle === "compact") {
+    return (
+      <div style={style}>
+        <div
+          className="flex items-center justify-between"
+          style={{ borderBottom: "1px solid #e5e7eb", paddingBottom: 8 }}
+        >
+          <div className="flex items-center gap-2">
+            <div
+              className="flex h-8 w-8 items-center justify-center rounded text-[10px] font-bold text-white"
+              style={{ backgroundColor: values.primaryColor }}
+            >
+              L
+            </div>
+            <div>
+              <p className="text-sm font-bold" style={{ color: values.primaryColor }}>
+                Your Workshop Name
+              </p>
+              <p className="text-[9px] text-gray-500">123 Main Street, City</p>
+            </div>
+          </div>
+          <div className="text-right">
+            <p className="text-sm font-bold">INVOICE</p>
+            <p className="text-[9px] text-gray-500">INV-1001</p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  if (headerStyle === "modern") {
+    return (
+      <div style={style}>
+        <div
+          className="rounded-md p-4 text-center text-white"
+          style={{ backgroundColor: values.primaryColor }}
+        >
+          <p className="text-lg font-bold">Your Workshop Name</p>
+          <p className="text-[9px] opacity-80">123 Main Street, City</p>
+          <div className="mt-1 flex justify-center gap-3 text-[8px] opacity-70">
+            <span>Tel: (555) 123-4567</span>
+            <span>shop@example.com</span>
+          </div>
+        </div>
+        <div className="mt-2 flex items-center justify-between">
+          <p className="text-sm font-bold">INVOICE</p>
+          <div className="flex gap-3 text-[9px] text-gray-500">
+            <span>INV-1001</span>
+            <span>Jan 15, 2026</span>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  // Standard
+  return (
+    <div style={style}>
+      <div
+        className="flex items-start justify-between"
+        style={{ borderBottom: `3px solid ${values.primaryColor}`, paddingBottom: 12 }}
+      >
+        <div>
+          <div
+            className="mb-1 flex h-8 w-8 items-center justify-center rounded text-[10px] font-bold text-white"
+            style={{ backgroundColor: values.primaryColor }}
+          >
+            Logo
+          </div>
+          <p className="text-sm font-bold" style={{ color: values.primaryColor }}>
+            Your Workshop Name
+          </p>
+          <p className="text-[9px] text-gray-500">123 Main Street, City</p>
+          <p className="text-[9px] text-gray-500">Tel: (555) 123-4567</p>
+        </div>
+        <div className="text-right">
+          <p className="text-lg font-bold tracking-tight" style={{ color: values.primaryColor }}>
+            INVOICE
+          </p>
+          <p className="text-[9px] text-gray-500">INV-1001</p>
+          <p className="text-[9px] text-gray-500">Jan 15, 2026</p>
+        </div>
+      </div>
+    </div>
+  );
+}
+
 export function TemplateSettings({
   initialValues,
 }: {
@@ -64,17 +163,120 @@ export function TemplateSettings({
     setSaving(false);
   };
 
+  const applyPreset = (presetId: string) => {
+    const preset = templatePresets.find((p) => p.id === presetId);
+    if (preset) {
+      setValues({
+        primaryColor: preset.primaryColor,
+        fontFamily: preset.fontFamily,
+        headerStyle: preset.headerStyle,
+      });
+    }
+  };
+
+  const currentPresetId = templatePresets.find(
+    (p) =>
+      p.primaryColor === values.primaryColor &&
+      p.fontFamily === values.fontFamily &&
+      p.headerStyle === values.headerStyle
+  )?.id;
+
   return (
     <div className="space-y-6">
       <ReadOnlyBanner />
       <div>
         <h2 className="text-lg font-semibold">Invoice Template</h2>
         <p className="text-sm text-muted-foreground">
-          Customize the appearance of your PDF invoices and quotes.
+          Choose a template preset or customize the appearance of your PDF invoices and quotes.
         </p>
       </div>
 
       <ReadOnlyWrapper>
+
+      {/* Template Gallery */}
+      <Card className="border-0 shadow-sm">
+        <CardHeader>
+          <CardTitle className="text-base">Template Presets</CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
+            {templatePresets.map((preset) => {
+              const isSelected = currentPresetId === preset.id;
+              return (
+                <button
+                  key={preset.id}
+                  type="button"
+                  onClick={() => applyPreset(preset.id)}
+                  className={`group relative rounded-lg border-2 p-3 text-left transition-all hover:shadow-md ${
+                    isSelected
+                      ? "border-primary shadow-sm"
+                      : "border-border hover:border-primary/50"
+                  }`}
+                >
+                  {isSelected && (
+                    <div className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
+                      <Check className="h-3 w-3" />
+                    </div>
+                  )}
+
+                  {/* Mini preview */}
+                  <div className="mb-2 overflow-hidden rounded border bg-white p-2">
+                    <div
+                      className="mb-1"
+                      style={
+                        preset.headerStyle === "modern"
+                          ? {
+                              backgroundColor: preset.primaryColor,
+                              borderRadius: 2,
+                              padding: "3px 4px",
+                            }
+                          : preset.headerStyle === "compact"
+                            ? {
+                                borderBottom: `1px solid #e5e7eb`,
+                                paddingBottom: 2,
+                              }
+                            : {
+                                borderBottom: `2px solid ${preset.primaryColor}`,
+                                paddingBottom: 2,
+                              }
+                      }
+                    >
+                      <div
+                        className="text-[6px] font-bold"
+                        style={{
+                          color:
+                            preset.headerStyle === "modern"
+                              ? "white"
+                              : preset.primaryColor,
+                          fontFamily: fontMap[preset.fontFamily] || "sans-serif",
+                        }}
+                      >
+                        Workshop
+                      </div>
+                    </div>
+                    <div className="space-y-0.5">
+                      <div className="h-1 w-3/4 rounded-full bg-gray-200" />
+                      <div className="h-1 w-1/2 rounded-full bg-gray-200" />
+                      <div
+                        className="mt-1 h-1.5 w-full rounded-sm"
+                        style={{ backgroundColor: `${preset.primaryColor}20` }}
+                      />
+                      <div className="h-1 w-full rounded-full bg-gray-100" />
+                      <div className="h-1 w-full rounded-full bg-gray-100" />
+                    </div>
+                  </div>
+
+                  <p className="text-xs font-medium">{preset.name}</p>
+                  <p className="text-[10px] text-muted-foreground leading-tight">
+                    {preset.description}
+                  </p>
+                </button>
+              );
+            })}
+          </div>
+        </CardContent>
+      </Card>
+
       <div className="grid gap-6 lg:grid-cols-2">
         {/* Color Settings */}
         <Card className="border-0 shadow-sm">
@@ -114,13 +316,6 @@ export function TemplateSettings({
                 </button>
               ))}
             </div>
-            <div className="rounded-lg border p-4">
-              <p className="text-xs text-muted-foreground mb-2">Preview</p>
-              <div className="flex items-center gap-3" style={{ borderBottom: `3px solid ${values.primaryColor}`, paddingBottom: 8 }}>
-                <span className="text-lg font-bold" style={{ color: values.primaryColor }}>Workshop Name</span>
-                <span className="ml-auto font-bold">INVOICE</span>
-              </div>
-            </div>
           </CardContent>
         </Card>
 
@@ -174,35 +369,14 @@ export function TemplateSettings({
             className="mx-auto max-w-[600px] rounded-lg border bg-white p-8 text-black"
             style={{ fontFamily: fontMap[values.fontFamily] || "sans-serif" }}
           >
-            {/* Header */}
-            <div
-              className="mb-6 flex items-start justify-between"
-              style={{ borderBottom: `3px solid ${values.primaryColor}`, paddingBottom: 16 }}
-            >
-              <div>
-                <div
-                  className="mb-2 flex h-10 w-10 items-center justify-center rounded text-sm font-bold text-white"
-                  style={{ backgroundColor: values.primaryColor }}
-                >
-                  Logo
-                </div>
-                <p className="text-lg font-bold" style={{ color: values.primaryColor }}>
-                  Your Workshop Name
-                </p>
-                <p className="text-xs text-gray-500">123 Main Street, City, State 12345</p>
-                <p className="text-xs text-gray-500">Phone: (555) 123-4567</p>
-              </div>
-              <div className="text-right">
-                <p className="text-2xl font-bold tracking-tight" style={{ color: values.primaryColor }}>
-                  INVOICE
-                </p>
-                <p className="text-xs text-gray-500">INV-1001</p>
-                <p className="text-xs text-gray-500">Date: Jan 15, 2026</p>
-              </div>
-            </div>
+            <PreviewHeader
+              values={values}
+              headerStyle={values.headerStyle}
+              fontFamily={values.fontFamily}
+            />
 
             {/* Bill To / Vehicle */}
-            <div className="mb-6 grid grid-cols-2 gap-6">
+            <div className="my-4 grid grid-cols-2 gap-4">
               <div>
                 <p className="mb-1 text-[10px] font-semibold uppercase tracking-wider" style={{ color: values.primaryColor }}>
                   Bill To
@@ -222,11 +396,11 @@ export function TemplateSettings({
             {/* Parts Table */}
             <table className="mb-1 w-full text-xs">
               <thead>
-                <tr style={{ backgroundColor: values.primaryColor }}>
-                  <th className="px-2 py-1.5 text-left font-medium text-white">Part</th>
-                  <th className="px-2 py-1.5 text-center font-medium text-white">Qty</th>
-                  <th className="px-2 py-1.5 text-right font-medium text-white">Price</th>
-                  <th className="px-2 py-1.5 text-right font-medium text-white">Total</th>
+                <tr style={{ backgroundColor: `${values.primaryColor}15` }}>
+                  <th className="px-2 py-1.5 text-left font-medium">Part</th>
+                  <th className="px-2 py-1.5 text-center font-medium">Qty</th>
+                  <th className="px-2 py-1.5 text-right font-medium">Price</th>
+                  <th className="px-2 py-1.5 text-right font-medium">Total</th>
                 </tr>
               </thead>
               <tbody>

+ 9 - 0
src/app/api/quotes/[id]/pdf/route.ts

@@ -67,6 +67,14 @@ export async function GET(
       torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
     }
 
+    const template = {
+      primaryColor: settingsMap["invoice.primaryColor"] || "#d97706",
+      fontFamily: settingsMap["invoice.fontFamily"] || "Helvetica",
+      showLogo: settingsMap["invoice.showLogo"] !== "false",
+      showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
+      headerStyle: settingsMap["invoice.headerStyle"] || "standard",
+    };
+
     const element = React.createElement(QuotePDF, {
       data: quote,
       workshop: {
@@ -80,6 +88,7 @@ export async function GET(
       torqvoiceLogoDataUri,
       dateFormat: settingsMap["workshop.dateFormat"] || undefined,
       timezone: settingsMap["workshop.timezone"] || undefined,
+      template,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     const buffer = await renderToBuffer(element);

+ 23 - 0
src/features/email/Actions/emailActions.ts

@@ -26,6 +26,11 @@ async function getWorkshopSettings(organizationId: string) {
             "workshop.currencyCode",
             "workshop.emailFromName",
             "workshop.emailEnabled",
+            "invoice.primaryColor",
+            "invoice.fontFamily",
+            "invoice.showLogo",
+            "invoice.showCompanyName",
+            "invoice.headerStyle",
           ],
         },
       },
@@ -91,6 +96,14 @@ export async function sendQuoteEmail(input: {
     const currencyCode = settings["workshop.currencyCode"] || "USD";
     const fromName = settings["workshop.emailFromName"] || settings["workshop.name"] || "Workshop";
 
+    const template = {
+      primaryColor: settings["invoice.primaryColor"] || "#d97706",
+      fontFamily: settings["invoice.fontFamily"] || "Helvetica",
+      showLogo: settings["invoice.showLogo"] !== "false",
+      showCompanyName: settings["invoice.showCompanyName"] !== "false",
+      headerStyle: settings["invoice.headerStyle"] || "standard",
+    };
+
     // Generate PDF
     const element = React.createElement(QuotePDF, {
       data: quote,
@@ -102,6 +115,7 @@ export async function sendQuoteEmail(input: {
       },
       currencyCode,
       logoDataUri,
+      template,
     }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
     const pdfBuffer = await renderToBuffer(element);
     const quoteNum = quote.quoteNumber || `QT-${quote.id.slice(-8).toUpperCase()}`;
@@ -176,6 +190,14 @@ export async function sendInvoiceEmail(input: {
     const currencyCode = settings["workshop.currencyCode"] || "USD";
     const fromName = settings["workshop.emailFromName"] || settings["workshop.name"] || "Workshop";
 
+    const invoiceTemplate = {
+      primaryColor: settings["invoice.primaryColor"] || "#d97706",
+      fontFamily: settings["invoice.fontFamily"] || "Helvetica",
+      showLogo: settings["invoice.showLogo"] !== "false",
+      showCompanyName: settings["invoice.showCompanyName"] !== "false",
+      headerStyle: settings["invoice.headerStyle"] || "standard",
+    };
+
     // Generate PDF
     const element = React.createElement(InvoicePDF, {
       data: record,
@@ -187,6 +209,7 @@ export async function sendInvoiceEmail(input: {
       },
       invoiceSettings: { currencyCode },
       logoDataUri,
+      template: invoiceTemplate,
     }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
     const pdfBuffer = await renderToBuffer(element);
     const invoiceNum = record.invoiceNumber || `INV-${record.id.slice(-8).toUpperCase()}`;

+ 198 - 123
src/features/quotes/Components/QuotePDF.tsx

@@ -1,94 +1,7 @@
-import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
-import { formatCurrency, getCurrencySymbol, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
-
-const amber = '#d97706'
-const amberLight = '#fef3c7'
-const gray = '#6b7280'
-const grayLight = '#f3f4f6'
-const dark = '#111827'
-
-const styles = StyleSheet.create({
-  page: { padding: 40, fontSize: 10, fontFamily: 'Helvetica', color: dark },
-  header: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    marginBottom: 30,
-    paddingBottom: 15,
-    borderBottomWidth: 3,
-    borderBottomColor: amber,
-  },
-  brandName: { fontSize: 22, fontFamily: 'Helvetica-Bold', color: amber },
-  brandSub: { fontSize: 9, color: gray, marginTop: 2 },
-  brandContact: { fontSize: 8, color: gray, marginTop: 1 },
-  invoiceTitle: { fontSize: 18, fontFamily: 'Helvetica-Bold', textAlign: 'right' as const },
-  invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
-  infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
-  infoBox: { flex: 1, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
-  infoLabel: {
-    fontSize: 8,
-    fontFamily: 'Helvetica-Bold',
-    color: amber,
-    textTransform: 'uppercase' as const,
-    marginBottom: 6,
-  },
-  infoText: { fontSize: 10, marginBottom: 2 },
-  infoTextBold: { fontSize: 10, fontFamily: 'Helvetica-Bold', marginBottom: 2 },
-  infoTextSmall: { fontSize: 9, color: gray, marginBottom: 2 },
-  sectionTitle: {
-    fontSize: 12,
-    fontFamily: 'Helvetica-Bold',
-    marginBottom: 8,
-    marginTop: 16,
-    color: dark,
-  },
-  table: { marginBottom: 4 },
-  tableHeader: {
-    flexDirection: 'row',
-    backgroundColor: amberLight,
-    paddingVertical: 6,
-    paddingHorizontal: 8,
-    borderRadius: 2,
-  },
-  tableRow: {
-    flexDirection: 'row',
-    paddingVertical: 5,
-    paddingHorizontal: 8,
-    borderBottomWidth: 0.5,
-    borderBottomColor: '#e5e7eb',
-  },
-  tableCell: { fontSize: 9 },
-  tableCellBold: { fontSize: 9, fontFamily: 'Helvetica-Bold' },
-  tableHeaderCell: { fontSize: 8, fontFamily: 'Helvetica-Bold', color: '#92400e' },
-  totalsBox: { marginTop: 16, marginLeft: 'auto', width: 220 },
-  totalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
-  totalLabel: { fontSize: 10, color: gray },
-  totalValue: { fontSize: 10 },
-  totalDivider: { borderTopWidth: 1, borderTopColor: amber, marginVertical: 4 },
-  grandTotalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
-  grandTotalLabel: { fontSize: 14, fontFamily: 'Helvetica-Bold' },
-  grandTotalValue: { fontSize: 14, fontFamily: 'Helvetica-Bold', color: amber },
-  notesSection: { marginTop: 20, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
-  notesLabel: {
-    fontSize: 8,
-    fontFamily: 'Helvetica-Bold',
-    color: amber,
-    textTransform: 'uppercase' as const,
-    marginBottom: 4,
-  },
-  notesText: { fontSize: 9, color: gray, lineHeight: 1.5 },
-  footer: {
-    position: 'absolute' as const,
-    bottom: 30,
-    left: 40,
-    right: 40,
-    textAlign: 'center' as const,
-    fontSize: 8,
-    color: gray,
-    paddingTop: 8,
-    borderTopWidth: 0.5,
-    borderTopColor: '#e5e7eb',
-  },
-})
+import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
+import { formatCurrency, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
+import { createStyles, gray, getFontBold } from '@/features/vehicles/Components/invoice-pdf/styles'
+import type { TemplateConfig } from '@/features/vehicles/Components/invoice-pdf/types'
 
 interface QuoteData {
   quoteNumber: string | null
@@ -143,6 +56,7 @@ export function QuotePDF({
   torqvoiceLogoDataUri,
   dateFormat,
   timezone,
+  template,
 }: {
   data: QuoteData
   workshop?: WorkshopInfo
@@ -151,46 +65,208 @@ export function QuotePDF({
   torqvoiceLogoDataUri?: string
   dateFormat?: string
   timezone?: string
+  template?: TemplateConfig
 }) {
-  const cs = getCurrencySymbol(currencyCode)
+  const primaryColor = template?.primaryColor || '#d97706'
+  const fontFamily = template?.fontFamily || 'Helvetica'
+  const headerStyle = template?.headerStyle || 'standard'
+  const showLogo = template?.showLogo !== false
+  const showCompanyName = template?.showCompanyName !== false
+  const styles = createStyles(primaryColor, fontFamily)
+  const fontBold = getFontBold(fontFamily)
+
   const quoteNum = data.quoteNumber || 'QUOTE'
   const df = dateFormat || DEFAULT_DATE_FORMAT
   const tz = timezone || undefined
   const createdDate = formatDateForPdf(data.createdAt, df, tz)
-  const validDate = data.validUntil
-    ? formatDateForPdf(data.validUntil, df, tz)
-    : null
+  const validDate = data.validUntil ? formatDateForPdf(data.validUntil, df, tz) : null
   const shopName = workshop?.name || 'Torqvoice'
 
-  return (
-    <Document>
-      <Page size="A4" style={styles.page}>
-        <View style={styles.header}>
-          <View>
-            {logoDataUri && (
-              <Image
-                src={logoDataUri}
-                style={{ width: 60, height: 60, marginBottom: 6, borderRadius: 4 }}
-              />
-            )}
-            <Text style={styles.brandName}>{shopName}</Text>
-            {workshop?.address && <Text style={styles.brandSub}>{workshop.address}</Text>}
-            {workshop?.phone && <Text style={styles.brandContact}>Tel: {workshop.phone}</Text>}
-            {workshop?.email && <Text style={styles.brandContact}>{workshop.email}</Text>}
+  const renderCompactHeader = () => (
+    <View style={{ marginBottom: 20 }}>
+      <View
+        style={{
+          flexDirection: 'row',
+          alignItems: 'center',
+          justifyContent: 'space-between',
+          paddingBottom: 10,
+          borderBottomWidth: 1,
+          borderBottomColor: '#e5e7eb',
+        }}
+      >
+        <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+          {showLogo && logoDataUri && (
+            <Image
+              src={logoDataUri}
+              style={{ maxWidth: 40, maxHeight: 40, borderRadius: 4, objectFit: 'contain' }}
+            />
+          )}
+          {showCompanyName && (
+            <View>
+              <Text style={{ fontSize: 16, fontFamily: fontBold, color: primaryColor }}>
+                {shopName}
+              </Text>
+              {workshop?.address && (
+                <Text style={{ fontSize: 8, color: gray }}>{workshop.address}</Text>
+              )}
+            </View>
+          )}
+        </View>
+        <View style={{ alignItems: 'flex-end' }}>
+          <Text style={{ fontSize: 14, fontFamily: fontBold, color: primaryColor }}>QUOTE</Text>
+          <Text style={{ fontSize: 9, color: gray, marginTop: 2 }}>{quoteNum}</Text>
+          <Text style={{ fontSize: 9, color: gray }}>{createdDate}</Text>
+          {validDate && <Text style={{ fontSize: 9, color: gray }}>Valid until: {validDate}</Text>}
+        </View>
+      </View>
+      <View
+        style={{
+          flexDirection: 'row',
+          justifyContent: 'space-between',
+          marginTop: 6,
+          paddingHorizontal: 2,
+        }}
+      >
+        <View style={{ flexDirection: 'row', gap: 12 }}>
+          {workshop?.phone && (
+            <Text style={{ fontSize: 8, color: gray }}>Tel: {workshop.phone}</Text>
+          )}
+          {workshop?.email && (
+            <Text style={{ fontSize: 8, color: gray }}>{workshop.email}</Text>
+          )}
+        </View>
+        {torqvoiceLogoDataUri && (
+          <View style={{ flexDirection: 'row', alignItems: 'center', gap: 3 }}>
+            <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+            <Text style={{ fontSize: 7, fontFamily: fontBold, color: gray }}>Torqvoice</Text>
           </View>
-          <View>
-            {torqvoiceLogoDataUri && (
-              <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginBottom: 6 }}>
-                <Image src={torqvoiceLogoDataUri} style={{ width: 16, height: 16 }} />
-                <Text style={{ fontSize: 9, fontFamily: 'Helvetica-Bold', color: gray }}>Torqvoice</Text>
-              </View>
+        )}
+      </View>
+    </View>
+  )
+
+  const renderModernHeader = () => (
+    <View style={{ marginBottom: 24 }}>
+      <View
+        style={{
+          backgroundColor: primaryColor,
+          padding: 20,
+          borderRadius: 4,
+          marginHorizontal: -10,
+        }}
+      >
+        <View
+          style={{
+            flexDirection: 'row',
+            alignItems: 'center',
+            justifyContent: 'center',
+            gap: 12,
+          }}
+        >
+          {showLogo && logoDataUri && (
+            <Image
+              src={logoDataUri}
+              style={{ maxWidth: 50, maxHeight: 50, borderRadius: 4, objectFit: 'contain' }}
+            />
+          )}
+          <View style={{ alignItems: 'center' }}>
+            {showCompanyName && (
+              <Text style={{ fontSize: 22, fontFamily: fontBold, color: 'white' }}>
+                {shopName}
+              </Text>
             )}
-            <Text style={{ ...styles.invoiceTitle, color: '#2563eb' }}>QUOTE</Text>
-            <Text style={styles.invoiceNumber}>{quoteNum}</Text>
-            <Text style={styles.invoiceNumber}>{createdDate}</Text>
-            {validDate && <Text style={styles.invoiceNumber}>Valid until: {validDate}</Text>}
+            {workshop?.address && (
+              <Text style={{ fontSize: 9, color: 'rgba(255,255,255,0.8)', marginTop: 2 }}>
+                {workshop.address}
+              </Text>
+            )}
+            <View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
+              {workshop?.phone && (
+                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                  Tel: {workshop.phone}
+                </Text>
+              )}
+              {workshop?.email && (
+                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                  {workshop.email}
+                </Text>
+              )}
+            </View>
           </View>
         </View>
+        {torqvoiceLogoDataUri && (
+          <View
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              justifyContent: 'center',
+              gap: 3,
+              marginTop: 8,
+            }}
+          >
+            <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+            <Text style={{ fontSize: 7, fontFamily: fontBold, color: 'rgba(255,255,255,0.7)' }}>
+              Torqvoice
+            </Text>
+          </View>
+        )}
+      </View>
+      <View
+        style={{
+          flexDirection: 'row',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          marginTop: 12,
+          paddingBottom: 8,
+        }}
+      >
+        <Text style={{ fontSize: 18, fontFamily: fontBold, color: primaryColor }}>QUOTE</Text>
+        <View style={{ flexDirection: 'row', gap: 16 }}>
+          <Text style={{ fontSize: 9, color: gray }}>{quoteNum}</Text>
+          <Text style={{ fontSize: 9, color: gray }}>{createdDate}</Text>
+          {validDate && <Text style={{ fontSize: 9, color: gray }}>Valid until: {validDate}</Text>}
+        </View>
+      </View>
+    </View>
+  )
+
+  const renderStandardHeader = () => (
+    <View style={styles.header}>
+      <View>
+        {showLogo && logoDataUri && (
+          <Image
+            src={logoDataUri}
+            style={{ width: 60, height: 60, marginBottom: 6, borderRadius: 4 }}
+          />
+        )}
+        {showCompanyName && <Text style={styles.brandName}>{shopName}</Text>}
+        {workshop?.address && <Text style={styles.brandSub}>{workshop.address}</Text>}
+        {workshop?.phone && <Text style={styles.brandContact}>Tel: {workshop.phone}</Text>}
+        {workshop?.email && <Text style={styles.brandContact}>{workshop.email}</Text>}
+      </View>
+      <View>
+        {torqvoiceLogoDataUri && (
+          <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginBottom: 6 }}>
+            <Image src={torqvoiceLogoDataUri} style={{ width: 16, height: 16 }} />
+            <Text style={{ fontSize: 9, fontFamily: fontBold, color: gray }}>Torqvoice</Text>
+          </View>
+        )}
+        <Text style={{ ...styles.invoiceTitle, color: primaryColor }}>QUOTE</Text>
+        <Text style={styles.invoiceNumber}>{quoteNum}</Text>
+        <Text style={styles.invoiceNumber}>{createdDate}</Text>
+        {validDate && <Text style={styles.invoiceNumber}>Valid until: {validDate}</Text>}
+      </View>
+    </View>
+  )
+
+  return (
+    <Document>
+      <Page size="A4" style={styles.page}>
+        {headerStyle === 'compact'
+          ? renderCompactHeader()
+          : headerStyle === 'modern'
+            ? renderModernHeader()
+            : renderStandardHeader()}
 
         <View style={styles.infoRow}>
           {data.customer && (
@@ -333,12 +409,11 @@ export function QuotePDF({
           </View>
         </View>
 
-        {/* Torqvoice branding near totals */}
         {torqvoiceLogoDataUri && (
           <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginTop: 6 }}>
             <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
             <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
-            <Text style={{ fontSize: 7, color: gray, fontFamily: 'Helvetica-Bold' }}>Torqvoice</Text>
+            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
           </View>
         )}
 
@@ -362,7 +437,7 @@ export function QuotePDF({
             </Text>
             <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
             <Image src={torqvoiceLogoDataUri} style={{ width: 14, height: 14 }} />
-            <Text style={{ fontSize: 7, color: gray, fontFamily: 'Helvetica-Bold' }}>Torqvoice</Text>
+            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
           </View>
         ) : (
           <Text style={styles.footer}>

+ 59 - 0
src/features/settings/Schema/templatePresets.ts

@@ -0,0 +1,59 @@
+export interface TemplatePreset {
+  id: string
+  name: string
+  description: string
+  primaryColor: string
+  fontFamily: string
+  headerStyle: string
+}
+
+export const templatePresets: TemplatePreset[] = [
+  {
+    id: 'standard',
+    name: 'Standard',
+    description: 'Classic amber theme with a traditional header layout',
+    primaryColor: '#d97706',
+    fontFamily: 'Helvetica',
+    headerStyle: 'standard',
+  },
+  {
+    id: 'professional',
+    name: 'Professional',
+    description: 'Sleek slate tones with a compact header',
+    primaryColor: '#475569',
+    fontFamily: 'Helvetica',
+    headerStyle: 'compact',
+  },
+  {
+    id: 'modern',
+    name: 'Modern',
+    description: 'Bold blue with a full-width colored banner',
+    primaryColor: '#2563eb',
+    fontFamily: 'Helvetica',
+    headerStyle: 'modern',
+  },
+  {
+    id: 'classic',
+    name: 'Classic',
+    description: 'Timeless serif font with dark red accents',
+    primaryColor: '#991b1b',
+    fontFamily: 'Times-Roman',
+    headerStyle: 'standard',
+  },
+  {
+    id: 'clean',
+    name: 'Clean',
+    description: 'Fresh emerald green with a modern banner',
+    primaryColor: '#059669',
+    fontFamily: 'Helvetica',
+    headerStyle: 'modern',
+  },
+  {
+    id: 'bold',
+    name: 'Bold',
+    description: 'Vibrant purple with a compact layout',
+    primaryColor: '#7c3aed',
+    fontFamily: 'Helvetica',
+    headerStyle: 'compact',
+  },
+]

+ 10 - 662
src/features/vehicles/Components/InvoicePDF.tsx

@@ -1,662 +1,10 @@
-import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
-import { formatCurrency, getCurrencySymbol, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
-
-interface TemplateConfig {
-  primaryColor?: string
-  fontFamily?: string
-  showLogo?: boolean
-  showCompanyName?: boolean
-  headerStyle?: string
-}
-
-function hexToRgb(hex: string) {
-  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
-  return result
-    ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }
-    : { r: 217, g: 119, b: 6 }
-}
-
-function lightenColor(hex: string, factor: number = 0.9) {
-  const { r, g, b } = hexToRgb(hex)
-  return `rgb(${Math.round(r + (255 - r) * factor)}, ${Math.round(g + (255 - g) * factor)}, ${Math.round(b + (255 - b) * factor)})`
-}
-
-function darkenColor(hex: string, factor: number = 0.3) {
-  const { r, g, b } = hexToRgb(hex)
-  return `rgb(${Math.round(r * (1 - factor))}, ${Math.round(g * (1 - factor))}, ${Math.round(b * (1 - factor))})`
-}
-
-const gray = '#6b7280'
-const grayLight = '#f3f4f6'
-const dark = '#111827'
-
-function createStyles(primary: string, font: string) {
-  const primaryLight = lightenColor(primary)
-  const primaryDark = darkenColor(primary)
-  const boldMap: Record<string, string> = {
-    'Times-Roman': 'Times-Bold',
-  }
-  const fontBold = boldMap[font] || `${font}-Bold`
-
-  return StyleSheet.create({
-    page: { padding: 40, fontSize: 10, fontFamily: font, color: dark },
-    header: {
-      flexDirection: 'row',
-      justifyContent: 'space-between',
-      marginBottom: 30,
-      paddingBottom: 15,
-      borderBottomWidth: 3,
-      borderBottomColor: primary,
-    },
-    brandName: { fontSize: 22, fontFamily: fontBold, color: primary },
-    brandSub: { fontSize: 9, color: gray, marginTop: 2 },
-    brandContact: { fontSize: 8, color: gray, marginTop: 1 },
-    invoiceTitle: { fontSize: 18, fontFamily: fontBold, textAlign: 'right' as const },
-    invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
-    infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
-    infoBox: { flex: 1, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
-    infoLabel: {
-      fontSize: 8,
-      fontFamily: fontBold,
-      color: primary,
-      textTransform: 'uppercase' as const,
-      marginBottom: 6,
-    },
-    infoText: { fontSize: 10, marginBottom: 2 },
-    infoTextBold: { fontSize: 10, fontFamily: fontBold, marginBottom: 2 },
-    infoTextSmall: { fontSize: 9, color: gray, marginBottom: 2 },
-    sectionTitle: {
-      fontSize: 12,
-      fontFamily: fontBold,
-      marginBottom: 8,
-      marginTop: 16,
-      color: dark,
-    },
-    table: { marginBottom: 4 },
-    tableHeader: {
-      flexDirection: 'row',
-      backgroundColor: primaryLight,
-      paddingVertical: 6,
-      paddingHorizontal: 8,
-      borderRadius: 2,
-    },
-    tableRow: {
-      flexDirection: 'row',
-      paddingVertical: 5,
-      paddingHorizontal: 8,
-      borderBottomWidth: 0.5,
-      borderBottomColor: '#e5e7eb',
-    },
-    tableCell: { fontSize: 9 },
-    tableCellBold: { fontSize: 9, fontFamily: fontBold },
-    tableHeaderCell: { fontSize: 8, fontFamily: fontBold, color: primaryDark },
-    totalsBox: { marginTop: 16, marginLeft: 'auto', width: 220 },
-    totalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
-    totalLabel: { fontSize: 10, color: gray },
-    totalValue: { fontSize: 10 },
-    totalDivider: { borderTopWidth: 1, borderTopColor: primary, marginVertical: 4 },
-    grandTotalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
-    grandTotalLabel: { fontSize: 14, fontFamily: fontBold },
-    grandTotalValue: { fontSize: 14, fontFamily: fontBold, color: primary },
-    notesSection: { marginTop: 20, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
-    notesLabel: {
-      fontSize: 8,
-      fontFamily: fontBold,
-      color: primary,
-      textTransform: 'uppercase' as const,
-      marginBottom: 4,
-    },
-    notesText: { fontSize: 9, color: gray, lineHeight: 1.5 },
-    attachmentImage: { maxWidth: 250, maxHeight: 200, marginTop: 8, borderRadius: 4 },
-    attachmentFileName: { fontSize: 8, color: gray, marginTop: 4, marginBottom: 8 },
-    footer: {
-      position: 'absolute' as const,
-      bottom: 30,
-      left: 40,
-      right: 40,
-      textAlign: 'center' as const,
-      fontSize: 8,
-      color: gray,
-      paddingTop: 8,
-      borderTopWidth: 0.5,
-      borderTopColor: '#e5e7eb',
-    },
-  })
-}
-
-interface InvoiceData {
-  id: string
-  title: string
-  description: string | null
-  type: string
-  serviceDate: Date
-  shopName: string | null
-  techName: string | null
-  mileage: number | null
-  diagnosticNotes: string | null
-  invoiceNotes: string | null
-  subtotal: number
-  taxRate: number
-  taxAmount: number
-  totalAmount: number
-  cost: number
-  invoiceNumber: string | null
-  discountType?: string | null
-  discountValue?: number
-  discountAmount?: number
-  partItems: {
-    partNumber: string | null
-    name: string
-    quantity: number
-    unitPrice: number
-    total: number
-  }[]
-  laborItems: {
-    description: string
-    hours: number
-    rate: number
-    total: number
-  }[]
-  vehicle: {
-    make: string
-    model: string
-    year: number
-    vin: string | null
-    licensePlate: string | null
-    mileage: number
-    customer: {
-      name: string
-      email: string | null
-      phone: string | null
-      address: string | null
-      company: string | null
-    } | null
-  }
-}
-
-interface WorkshopInfo {
-  name: string
-  address: string
-  phone: string
-  email: string
-}
-
-interface InvoiceSettingsProps {
-  bankAccount?: string
-  orgNumber?: string
-  paymentTerms?: string
-  footerNote?: string
-  showBankAccount?: boolean
-  showOrgNumber?: boolean
-  dueDays?: number
-  currencyCode?: string
-  unitSystem?: string
-  dateFormat?: string
-  timezone?: string
-}
-
-interface PaymentSummary {
-  totalPaid: number
-  payments: { amount: number; date: string; method: string }[]
-}
-
-interface ImageAttachment {
-  fileName: string
-  dataUri: string
-  description?: string
-}
-
-interface OtherAttachment {
-  fileName: string
-  fileType: string
-}
-
-export function InvoicePDF({
-  data,
-  workshop,
-  invoiceSettings,
-  paymentSummary,
-  imageAttachments = [],
-  otherAttachments = [],
-  pdfAttachmentNames = [],
-  logoDataUri,
-  template,
-  torqvoiceLogoDataUri,
-}: {
-  data: InvoiceData
-  workshop?: WorkshopInfo
-  invoiceSettings?: InvoiceSettingsProps
-  paymentSummary?: PaymentSummary
-  imageAttachments?: ImageAttachment[]
-  otherAttachments?: OtherAttachment[]
-  pdfAttachmentNames?: string[]
-  logoDataUri?: string
-  template?: TemplateConfig
-  torqvoiceLogoDataUri?: string
-}) {
-  const primaryColor = template?.primaryColor || '#d97706'
-  const fontFamily = template?.fontFamily || 'Helvetica'
-  const showLogo = template?.showLogo !== false
-  const showCompanyName = template?.showCompanyName !== false
-  const styles = createStyles(primaryColor, fontFamily)
-
-  const cs = getCurrencySymbol(invoiceSettings?.currencyCode || 'USD')
-  const cc = invoiceSettings?.currencyCode || 'USD'
-  const vehicleName = `${data.vehicle.year} ${data.vehicle.make} ${data.vehicle.model}`
-  const partsSubtotal = data.partItems.reduce((sum, p) => sum + p.total, 0)
-  const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
-  const displayTotal = data.totalAmount > 0 ? data.totalAmount : data.cost
-  const invoiceNum = data.invoiceNumber || `INV-${data.id.slice(-8).toUpperCase()}`
-  const df = invoiceSettings?.dateFormat || DEFAULT_DATE_FORMAT
-  const tz = invoiceSettings?.timezone || undefined
-  const serviceDate = formatDateForPdf(data.serviceDate, df, tz)
-
-  const dueDays = invoiceSettings?.dueDays || 0
-  const dueDate =
-    dueDays > 0
-      ? formatDateForPdf(
-          new Date(new Date(data.serviceDate).getTime() + dueDays * 86400000),
-          df,
-          tz,
-        )
-      : null
-
-  const balanceDue = paymentSummary ? displayTotal - paymentSummary.totalPaid : displayTotal
-  const isPaidInFull = paymentSummary && paymentSummary.totalPaid >= displayTotal
-
-  const shopDisplayName = workshop?.name || data.shopName || 'Torqvoice'
-  const hasAttachments = imageAttachments.length > 0 || otherAttachments.length > 0
-  const fontBold = fontFamily === 'Times-Roman' ? 'Times-Bold' : `${fontFamily}-Bold`
-
-  return (
-    <Document>
-      <Page size="A4" style={styles.page}>
-        {/* Header */}
-        <View style={styles.header}>
-          <View>
-            {showLogo && logoDataUri && (
-              <Image
-                src={logoDataUri}
-                style={{
-                  maxWidth: 150,
-                  maxHeight: 60,
-                  marginBottom: 6,
-                  borderRadius: 4,
-                  objectFit: 'contain',
-                  objectPosition: 'left',
-                }}
-              />
-            )}
-            {showCompanyName && <Text style={styles.brandName}>{shopDisplayName}</Text>}
-            {workshop?.address ? (
-              <Text style={styles.brandSub}>{workshop.address}</Text>
-            ) : (
-              <Text style={styles.brandSub}>Professional Workshop Management</Text>
-            )}
-            {workshop?.phone && <Text style={styles.brandContact}>Tel: {workshop.phone}</Text>}
-            {workshop?.email && <Text style={styles.brandContact}>{workshop.email}</Text>}
-            {invoiceSettings?.showOrgNumber && invoiceSettings?.orgNumber && (
-              <Text style={styles.brandContact}>Org: {invoiceSettings.orgNumber}</Text>
-            )}
-            {torqvoiceLogoDataUri && (
-              <View
-                style={{
-                  flexDirection: 'row',
-                  alignItems: 'flex-start',
-                  justifyContent: 'flex-start',
-                  gap: 3,
-                  marginBottom: 6,
-                }}
-              >
-                <Image src={torqvoiceLogoDataUri} style={{ width: 16, height: 16 }} />
-                <Text style={{ fontSize: 9, fontFamily: fontBold, color: gray }}>Torqvoice</Text>
-              </View>
-            )}
-          </View>
-          <View>
-            <Text style={styles.invoiceTitle}>INVOICE</Text>
-            <Text style={styles.invoiceNumber}>{invoiceNum}</Text>
-            <Text style={styles.invoiceNumber}>{serviceDate}</Text>
-            {dueDate && <Text style={styles.invoiceNumber}>Due: {dueDate}</Text>}
-          </View>
-        </View>
-
-        {/* Customer & Vehicle Info */}
-        <View style={styles.infoRow}>
-          {data.vehicle.customer && (
-            <View style={styles.infoBox}>
-              <Text style={styles.infoLabel}>Bill To</Text>
-              <Text style={styles.infoTextBold}>{data.vehicle.customer.name}</Text>
-              {data.vehicle.customer.company && (
-                <Text style={styles.infoText}>{data.vehicle.customer.company}</Text>
-              )}
-              {data.vehicle.customer.address && (
-                <Text style={styles.infoTextSmall}>{data.vehicle.customer.address}</Text>
-              )}
-              {data.vehicle.customer.email && (
-                <Text style={styles.infoTextSmall}>{data.vehicle.customer.email}</Text>
-              )}
-              {data.vehicle.customer.phone && (
-                <Text style={styles.infoTextSmall}>{data.vehicle.customer.phone}</Text>
-              )}
-            </View>
-          )}
-          <View style={styles.infoBox}>
-            <Text style={styles.infoLabel}>Vehicle</Text>
-            <Text style={styles.infoTextBold}>{vehicleName}</Text>
-            {data.vehicle.vin && <Text style={styles.infoTextSmall}>VIN: {data.vehicle.vin}</Text>}
-            {data.vehicle.licensePlate && (
-              <Text style={styles.infoTextSmall}>Plate: {data.vehicle.licensePlate}</Text>
-            )}
-            {data.mileage && (
-              <Text style={styles.infoTextSmall}>
-                Mileage: {data.mileage.toLocaleString()}{' '}
-                {invoiceSettings?.unitSystem === 'metric' ? 'km' : 'mi'}
-              </Text>
-            )}
-          </View>
-          <View style={styles.infoBox}>
-            <Text style={styles.infoLabel}>Service</Text>
-            <Text style={styles.infoTextBold}>{data.title}</Text>
-            <Text style={styles.infoTextSmall}>Type: {data.type}</Text>
-            {data.techName && <Text style={styles.infoTextSmall}>Tech: {data.techName}</Text>}
-          </View>
-        </View>
-
-        {/* Parts Table */}
-        {data.partItems.length > 0 && (
-          <View>
-            <Text style={styles.sectionTitle}>Parts</Text>
-            <View style={styles.table}>
-              <View style={styles.tableHeader}>
-                <Text style={{ ...styles.tableHeaderCell, width: '15%' }}>Part #</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '35%' }}>Description</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '12%', textAlign: 'right' }}>
-                  Qty
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '18%', textAlign: 'right' }}>
-                  Unit Price
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  Total
-                </Text>
-              </View>
-              {data.partItems.map((part, i) => (
-                <View key={i} style={styles.tableRow}>
-                  <Text style={{ ...styles.tableCell, width: '15%' }}>
-                    {part.partNumber || '-'}
-                  </Text>
-                  <Text style={{ ...styles.tableCell, width: '35%' }}>{part.name}</Text>
-                  <Text style={{ ...styles.tableCell, width: '12%', textAlign: 'right' }}>
-                    {part.quantity}
-                  </Text>
-                  <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right' }}>
-                    {formatCurrency(part.unitPrice, cc)}
-                  </Text>
-                  <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
-                    {formatCurrency(part.total, cc)}
-                  </Text>
-                </View>
-              ))}
-            </View>
-          </View>
-        )}
-
-        {/* Labor Table */}
-        {data.laborItems.length > 0 && (
-          <View>
-            <Text style={styles.sectionTitle}>Labor</Text>
-            <View style={styles.table}>
-              <View style={styles.tableHeader}>
-                <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>Description</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>
-                  Hours
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  Rate
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  Total
-                </Text>
-              </View>
-              {data.laborItems.map((labor, i) => (
-                <View key={i} style={styles.tableRow}>
-                  <Text style={{ ...styles.tableCell, width: '45%' }}>{labor.description}</Text>
-                  <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right' }}>
-                    {labor.hours}
-                  </Text>
-                  <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right' }}>
-                    {formatCurrency(labor.rate, cc)}/hr
-                  </Text>
-                  <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
-                    {formatCurrency(labor.total, cc)}
-                  </Text>
-                </View>
-              ))}
-            </View>
-          </View>
-        )}
-
-        {/* Invoice Notes */}
-        {data.invoiceNotes && (
-          <View style={styles.notesSection}>
-            <Text style={styles.notesLabel}>Notes</Text>
-            <Text style={styles.notesText}>{data.invoiceNotes}</Text>
-          </View>
-        )}
-
-        {/* Totals */}
-        <View style={styles.totalsBox}>
-          {data.partItems.length > 0 && (
-            <View style={styles.totalRow}>
-              <Text style={styles.totalLabel}>Parts</Text>
-              <Text style={styles.totalValue}>{formatCurrency(partsSubtotal, cc)}</Text>
-            </View>
-          )}
-          {data.laborItems.length > 0 && (
-            <View style={styles.totalRow}>
-              <Text style={styles.totalLabel}>Labor</Text>
-              <Text style={styles.totalValue}>{formatCurrency(laborSubtotal, cc)}</Text>
-            </View>
-          )}
-          {data.subtotal > 0 && (
-            <View style={styles.totalRow}>
-              <Text style={styles.totalLabel}>Subtotal</Text>
-              <Text style={styles.totalValue}>{formatCurrency(data.subtotal, cc)}</Text>
-            </View>
-          )}
-          {(data.discountAmount ?? 0) > 0 && (
-            <View style={styles.totalRow}>
-              <Text style={styles.totalLabel}>
-                Discount{data.discountType === 'percentage' ? ` (${data.discountValue}%)` : ''}
-              </Text>
-              <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
-                {formatCurrency(-(data.discountAmount ?? 0), cc)}
-              </Text>
-            </View>
-          )}
-          {data.taxRate > 0 && (
-            <View style={styles.totalRow}>
-              <Text style={styles.totalLabel}>Tax ({data.taxRate}%)</Text>
-              <Text style={styles.totalValue}>{formatCurrency(data.taxAmount, cc)}</Text>
-            </View>
-          )}
-          <View style={styles.totalDivider} />
-          {paymentSummary && paymentSummary.payments.length > 0 ? (
-            <>
-              {/* Smaller total when payments exist */}
-              <View style={styles.totalRow}>
-                <Text style={{ fontSize: 10, fontFamily: fontBold }}>Total</Text>
-                <Text style={{ fontSize: 10, fontFamily: fontBold }}>
-                  {formatCurrency(displayTotal, cc)}
-                </Text>
-              </View>
-              <View style={{ ...styles.totalDivider, marginVertical: 6 }} />
-              <Text style={{ fontSize: 9, fontFamily: fontBold, marginBottom: 4 }}>
-                Payments Received
-              </Text>
-              {paymentSummary.payments.map((p, i) => (
-                <View key={i} style={styles.totalRow}>
-                  <Text style={{ fontSize: 8, color: gray }}>
-                    {p.date} ({p.method})
-                  </Text>
-                  <Text style={{ fontSize: 9, color: '#059669' }}>
-                    {formatCurrency(-p.amount, cc)}
-                  </Text>
-                </View>
-              ))}
-              <View
-                style={{ borderTopWidth: 2, borderTopColor: primaryColor, marginVertical: 6 }}
-              />
-              <View
-                style={{
-                  flexDirection: 'row',
-                  justifyContent: 'space-between',
-                  paddingVertical: 6,
-                  backgroundColor: isPaidInFull ? '#f0fdf4' : lightenColor(primaryColor, 0.92),
-                  marginHorizontal: -8,
-                  paddingHorizontal: 8,
-                  borderRadius: 3,
-                }}
-              >
-                <Text style={{ fontSize: 16, fontFamily: fontBold }}>
-                  {isPaidInFull ? 'PAID IN FULL' : 'Amount Due'}
-                </Text>
-                <Text
-                  style={{
-                    fontSize: 16,
-                    fontFamily: fontBold,
-                    color: isPaidInFull ? '#059669' : primaryColor,
-                  }}
-                >
-                  {isPaidInFull ? '' : formatCurrency(balanceDue, cc)}
-                </Text>
-              </View>
-            </>
-          ) : (
-            <View style={styles.grandTotalRow}>
-              <Text style={styles.grandTotalLabel}>Total</Text>
-              <Text style={styles.grandTotalValue}>{formatCurrency(displayTotal, cc)}</Text>
-            </View>
-          )}
-        </View>
-
-        {/* Torqvoice branding near totals */}
-        {torqvoiceLogoDataUri && (
-          <View
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-              justifyContent: 'flex-start',
-              gap: 3,
-              marginTop: 6,
-            }}
-          >
-            <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
-            <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
-            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
-          </View>
-        )}
-
-        {/* Payment Terms */}
-        {invoiceSettings?.paymentTerms && (
-          <View style={{ marginTop: 8 }}>
-            <Text style={{ fontSize: 9, color: gray }}>
-              Payment Terms: {invoiceSettings.paymentTerms}
-            </Text>
-          </View>
-        )}
-
-        {/* Bank Account / Til Konto */}
-        {invoiceSettings?.showBankAccount && invoiceSettings?.bankAccount && (
-          <View style={{ ...styles.notesSection, marginTop: 12 }}>
-            <Text style={styles.notesLabel}>Til Konto / Bank Account</Text>
-            <Text style={{ fontSize: 11, fontFamily: fontBold }}>
-              {invoiceSettings.bankAccount}
-            </Text>
-          </View>
-        )}
-
-        {/* Diagnostic Notes */}
-        {data.diagnosticNotes && (
-          <View style={{ ...styles.notesSection, marginTop: 8 }}>
-            <Text style={styles.notesLabel}>Diagnostic Notes</Text>
-            <Text style={styles.notesText}>{data.diagnosticNotes}</Text>
-          </View>
-        )}
-
-        {/* Attached Files Reference */}
-        {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
-          <View style={{ ...styles.notesSection, marginTop: 8 }}>
-            <Text style={styles.notesLabel}>Attached Documents</Text>
-            {pdfAttachmentNames.map((name, i) => (
-              <Text key={`pdf-${i}`} style={styles.notesText}>
-                {name} (see appended pages)
-              </Text>
-            ))}
-            {otherAttachments.map((att, i) => (
-              <Text key={i} style={styles.notesText}>
-                {att.fileName}
-              </Text>
-            ))}
-          </View>
-        )}
-
-        {/* Footer */}
-        {torqvoiceLogoDataUri ? (
-          <View
-            style={{
-              ...styles.footer,
-              flexDirection: 'row',
-              alignItems: 'center',
-              justifyContent: 'center',
-              gap: 4,
-            }}
-          >
-            {invoiceSettings?.footerNote ? (
-              <Text style={{ fontSize: 8, color: gray }}>{invoiceSettings.footerNote} · </Text>
-            ) : null}
-            <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
-            <Image src={torqvoiceLogoDataUri} style={{ width: 14, height: 14 }} />
-            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
-          </View>
-        ) : (
-          <Text style={styles.footer}>
-            {invoiceSettings?.footerNote ||
-              `${shopDisplayName} · ${serviceDate}`}
-          </Text>
-        )}
-      </Page>
-
-      {/* Diagnostic Images Page */}
-      {hasAttachments && imageAttachments.length > 0 && (
-        <Page size="A4" style={styles.page}>
-          <Text style={styles.sectionTitle}>Diagnostic Report Images</Text>
-          <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
-            {imageAttachments.map((img, i) => (
-              <View key={i} style={{ width: '48%', marginBottom: 8 }}>
-                <Image
-                  src={img.dataUri}
-                  style={{
-                    maxHeight: 250,
-                    borderRadius: 4,
-                    objectFit: 'contain',
-                    objectPosition: 'left',
-                  }}
-                />
-                {img.description ? (
-                  <Text style={{ fontSize: 8, color: gray, marginTop: 2 }}>{img.description}</Text>
-                ) : (
-                  <Text style={styles.attachmentFileName}>{img.fileName}</Text>
-                )}
-              </View>
-            ))}
-          </View>
-          <Text style={styles.footer}>
-            {shopDisplayName} · {invoiceNum}
-          </Text>
-        </Page>
-      )}
-    </Document>
-  )
-}
+export { InvoicePDF } from './invoice-pdf'
+export type {
+  TemplateConfig,
+  InvoiceData,
+  WorkshopInfo,
+  InvoiceSettingsProps,
+  PaymentSummary,
+  ImageAttachment,
+  OtherAttachment,
+} from './invoice-pdf'

+ 78 - 0
src/features/vehicles/Components/invoice-pdf/Footer.tsx

@@ -0,0 +1,78 @@
+import { Text, View, Image } from '@react-pdf/renderer'
+import type { InvoiceSettingsProps } from './types'
+import { gray, getFontBold } from './styles'
+import type { Style } from '@react-pdf/types'
+
+interface FooterProps {
+  shopDisplayName: string
+  serviceDate: string
+  invoiceSettings?: InvoiceSettingsProps
+  invoiceNum: string
+  primaryColor: string
+  fontFamily: string
+  torqvoiceLogoDataUri?: string
+  paymentTerms?: string
+  styles: Record<string, Style>
+}
+
+export function Footer({
+  shopDisplayName,
+  serviceDate,
+  invoiceSettings,
+  fontFamily,
+  torqvoiceLogoDataUri,
+  styles,
+}: FooterProps) {
+  const fontBold = getFontBold(fontFamily)
+
+  return (
+    <>
+      {invoiceSettings?.paymentTerms && (
+        <View style={{ marginTop: 8 }}>
+          <Text style={{ fontSize: 9, color: gray }}>
+            Payment Terms: {invoiceSettings.paymentTerms}
+          </Text>
+        </View>
+      )}
+
+      {torqvoiceLogoDataUri ? (
+        <View
+          style={{
+            ...styles.footer,
+            flexDirection: 'row',
+            alignItems: 'center',
+            justifyContent: 'center',
+            gap: 4,
+          }}
+        >
+          {invoiceSettings?.footerNote ? (
+            <Text style={{ fontSize: 8, color: gray }}>{invoiceSettings.footerNote} · </Text>
+          ) : null}
+          <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
+          <Image src={torqvoiceLogoDataUri} style={{ width: 14, height: 14 }} />
+          <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
+        </View>
+      ) : (
+        <Text style={styles.footer}>
+          {invoiceSettings?.footerNote || `${shopDisplayName} · ${serviceDate}`}
+        </Text>
+      )}
+    </>
+  )
+}
+
+export function AttachmentsFooter({
+  shopDisplayName,
+  invoiceNum,
+  styles,
+}: {
+  shopDisplayName: string
+  invoiceNum: string
+  styles: Record<string, Style>
+}) {
+  return (
+    <Text style={styles.footer}>
+      {shopDisplayName} · {invoiceNum}
+    </Text>
+  )
+}

+ 257 - 0
src/features/vehicles/Components/invoice-pdf/Header.tsx

@@ -0,0 +1,257 @@
+import { Text, View, Image } from '@react-pdf/renderer'
+import type { WorkshopInfo, InvoiceSettingsProps } from './types'
+import { gray, getFontBold } from './styles'
+import type { Style } from '@react-pdf/types'
+
+interface HeaderProps {
+  headerStyle: string
+  primaryColor: string
+  fontFamily: string
+  showLogo: boolean
+  showCompanyName: boolean
+  logoDataUri?: string
+  torqvoiceLogoDataUri?: string
+  workshop?: WorkshopInfo
+  invoiceSettings?: InvoiceSettingsProps
+  shopDisplayName: string
+  invoiceNum: string
+  serviceDate: string
+  dueDate: string | null
+  styles: Record<string, Style>
+}
+
+export function Header({
+  headerStyle,
+  primaryColor,
+  fontFamily,
+  showLogo,
+  showCompanyName,
+  logoDataUri,
+  torqvoiceLogoDataUri,
+  workshop,
+  invoiceSettings,
+  shopDisplayName,
+  invoiceNum,
+  serviceDate,
+  dueDate,
+  styles,
+}: HeaderProps) {
+  const fontBold = getFontBold(fontFamily)
+
+  if (headerStyle === 'compact') {
+    return (
+      <View style={{ marginBottom: 20 }}>
+        <View
+          style={{
+            flexDirection: 'row',
+            alignItems: 'center',
+            justifyContent: 'space-between',
+            paddingBottom: 10,
+            borderBottomWidth: 1,
+            borderBottomColor: '#e5e7eb',
+          }}
+        >
+          <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+            {showLogo && logoDataUri && (
+              <Image
+                src={logoDataUri}
+                style={{ maxWidth: 40, maxHeight: 40, borderRadius: 4, objectFit: 'contain' }}
+              />
+            )}
+            {showCompanyName && (
+              <View>
+                <Text style={{ fontSize: 16, fontFamily: fontBold, color: primaryColor }}>
+                  {shopDisplayName}
+                </Text>
+                {workshop?.address && (
+                  <Text style={{ fontSize: 8, color: gray }}>{workshop.address}</Text>
+                )}
+              </View>
+            )}
+          </View>
+          <View style={{ alignItems: 'flex-end' }}>
+            <Text style={{ fontSize: 14, fontFamily: fontBold }}>INVOICE</Text>
+            <Text style={{ fontSize: 9, color: gray, marginTop: 2 }}>{invoiceNum}</Text>
+            <Text style={{ fontSize: 9, color: gray }}>{serviceDate}</Text>
+            {dueDate && <Text style={{ fontSize: 9, color: gray }}>Due: {dueDate}</Text>}
+          </View>
+        </View>
+        <View
+          style={{
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+            marginTop: 6,
+            paddingHorizontal: 2,
+          }}
+        >
+          <View style={{ flexDirection: 'row', gap: 12 }}>
+            {workshop?.phone && (
+              <Text style={{ fontSize: 8, color: gray }}>Tel: {workshop.phone}</Text>
+            )}
+            {workshop?.email && (
+              <Text style={{ fontSize: 8, color: gray }}>{workshop.email}</Text>
+            )}
+            {invoiceSettings?.showOrgNumber && invoiceSettings?.orgNumber && (
+              <Text style={{ fontSize: 8, color: gray }}>Org: {invoiceSettings.orgNumber}</Text>
+            )}
+          </View>
+          {torqvoiceLogoDataUri && (
+            <View style={{ flexDirection: 'row', alignItems: 'center', gap: 3 }}>
+              <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+              <Text style={{ fontSize: 7, fontFamily: fontBold, color: gray }}>Torqvoice</Text>
+            </View>
+          )}
+        </View>
+      </View>
+    )
+  }
+
+  if (headerStyle === 'modern') {
+    return (
+      <View style={{ marginBottom: 24 }}>
+        <View
+          style={{
+            backgroundColor: primaryColor,
+            padding: 20,
+            borderRadius: 4,
+            marginHorizontal: -10,
+          }}
+        >
+          <View
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              justifyContent: 'center',
+              gap: 12,
+            }}
+          >
+            {showLogo && logoDataUri && (
+              <Image
+                src={logoDataUri}
+                style={{
+                  maxWidth: 50,
+                  maxHeight: 50,
+                  borderRadius: 4,
+                  objectFit: 'contain',
+                }}
+              />
+            )}
+            <View style={{ alignItems: 'center' }}>
+              {showCompanyName && (
+                <Text style={{ fontSize: 22, fontFamily: fontBold, color: 'white' }}>
+                  {shopDisplayName}
+                </Text>
+              )}
+              {workshop?.address && (
+                <Text style={{ fontSize: 9, color: 'rgba(255,255,255,0.8)', marginTop: 2 }}>
+                  {workshop.address}
+                </Text>
+              )}
+              <View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
+                {workshop?.phone && (
+                  <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                    Tel: {workshop.phone}
+                  </Text>
+                )}
+                {workshop?.email && (
+                  <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                    {workshop.email}
+                  </Text>
+                )}
+                {invoiceSettings?.showOrgNumber && invoiceSettings?.orgNumber && (
+                  <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                    Org: {invoiceSettings.orgNumber}
+                  </Text>
+                )}
+              </View>
+            </View>
+          </View>
+          {torqvoiceLogoDataUri && (
+            <View
+              style={{
+                flexDirection: 'row',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                marginTop: 8,
+              }}
+            >
+              <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+              <Text style={{ fontSize: 7, fontFamily: fontBold, color: 'rgba(255,255,255,0.7)' }}>
+                Torqvoice
+              </Text>
+            </View>
+          )}
+        </View>
+        <View
+          style={{
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+            alignItems: 'center',
+            marginTop: 12,
+            paddingBottom: 8,
+          }}
+        >
+          <Text style={{ fontSize: 18, fontFamily: fontBold }}>INVOICE</Text>
+          <View style={{ flexDirection: 'row', gap: 16 }}>
+            <Text style={{ fontSize: 9, color: gray }}>{invoiceNum}</Text>
+            <Text style={{ fontSize: 9, color: gray }}>{serviceDate}</Text>
+            {dueDate && <Text style={{ fontSize: 9, color: gray }}>Due: {dueDate}</Text>}
+          </View>
+        </View>
+      </View>
+    )
+  }
+
+  // Standard header (default)
+  return (
+    <View style={styles.header}>
+      <View>
+        {showLogo && logoDataUri && (
+          <Image
+            src={logoDataUri}
+            style={{
+              maxWidth: 150,
+              maxHeight: 60,
+              marginBottom: 6,
+              borderRadius: 4,
+              objectFit: 'contain',
+              objectPosition: 'left',
+            }}
+          />
+        )}
+        {showCompanyName && <Text style={styles.brandName}>{shopDisplayName}</Text>}
+        {workshop?.address ? (
+          <Text style={styles.brandSub}>{workshop.address}</Text>
+        ) : (
+          <Text style={styles.brandSub}>Professional Workshop Management</Text>
+        )}
+        {workshop?.phone && <Text style={styles.brandContact}>Tel: {workshop.phone}</Text>}
+        {workshop?.email && <Text style={styles.brandContact}>{workshop.email}</Text>}
+        {invoiceSettings?.showOrgNumber && invoiceSettings?.orgNumber && (
+          <Text style={styles.brandContact}>Org: {invoiceSettings.orgNumber}</Text>
+        )}
+        {torqvoiceLogoDataUri && (
+          <View
+            style={{
+              flexDirection: 'row',
+              alignItems: 'flex-start',
+              justifyContent: 'flex-start',
+              gap: 3,
+              marginBottom: 6,
+            }}
+          >
+            <Image src={torqvoiceLogoDataUri} style={{ width: 16, height: 16 }} />
+            <Text style={{ fontSize: 9, fontFamily: fontBold, color: gray }}>Torqvoice</Text>
+          </View>
+        )}
+      </View>
+      <View>
+        <Text style={styles.invoiceTitle}>INVOICE</Text>
+        <Text style={styles.invoiceNumber}>{invoiceNum}</Text>
+        <Text style={styles.invoiceNumber}>{serviceDate}</Text>
+        {dueDate && <Text style={styles.invoiceNumber}>Due: {dueDate}</Text>}
+      </View>
+    </View>
+  )
+}

+ 55 - 0
src/features/vehicles/Components/invoice-pdf/InfoSection.tsx

@@ -0,0 +1,55 @@
+import { Text, View } from '@react-pdf/renderer'
+import type { InvoiceData, InvoiceSettingsProps } from './types'
+import type { Style } from '@react-pdf/types'
+
+interface InfoSectionProps {
+  data: InvoiceData
+  vehicleName: string
+  invoiceSettings?: InvoiceSettingsProps
+  styles: Record<string, Style>
+}
+
+export function InfoSection({ data, vehicleName, invoiceSettings, styles }: InfoSectionProps) {
+  return (
+    <View style={styles.infoRow}>
+      {data.vehicle.customer && (
+        <View style={styles.infoBox}>
+          <Text style={styles.infoLabel}>Bill To</Text>
+          <Text style={styles.infoTextBold}>{data.vehicle.customer.name}</Text>
+          {data.vehicle.customer.company && (
+            <Text style={styles.infoText}>{data.vehicle.customer.company}</Text>
+          )}
+          {data.vehicle.customer.address && (
+            <Text style={styles.infoTextSmall}>{data.vehicle.customer.address}</Text>
+          )}
+          {data.vehicle.customer.email && (
+            <Text style={styles.infoTextSmall}>{data.vehicle.customer.email}</Text>
+          )}
+          {data.vehicle.customer.phone && (
+            <Text style={styles.infoTextSmall}>{data.vehicle.customer.phone}</Text>
+          )}
+        </View>
+      )}
+      <View style={styles.infoBox}>
+        <Text style={styles.infoLabel}>Vehicle</Text>
+        <Text style={styles.infoTextBold}>{vehicleName}</Text>
+        {data.vehicle.vin && <Text style={styles.infoTextSmall}>VIN: {data.vehicle.vin}</Text>}
+        {data.vehicle.licensePlate && (
+          <Text style={styles.infoTextSmall}>Plate: {data.vehicle.licensePlate}</Text>
+        )}
+        {data.mileage && (
+          <Text style={styles.infoTextSmall}>
+            Mileage: {data.mileage.toLocaleString()}{' '}
+            {invoiceSettings?.unitSystem === 'metric' ? 'km' : 'mi'}
+          </Text>
+        )}
+      </View>
+      <View style={styles.infoBox}>
+        <Text style={styles.infoLabel}>Service</Text>
+        <Text style={styles.infoTextBold}>{data.title}</Text>
+        <Text style={styles.infoTextSmall}>Type: {data.type}</Text>
+        {data.techName && <Text style={styles.infoTextSmall}>Tech: {data.techName}</Text>}
+      </View>
+    </View>
+  )
+}

+ 198 - 0
src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx

@@ -0,0 +1,198 @@
+import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
+import { formatCurrency, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
+import { createStyles, gray, getFontBold, lightenColor } from './styles'
+import { Header } from './Header'
+import { InfoSection } from './InfoSection'
+import { PartsTable, LaborTable } from './Tables'
+import { Totals } from './Totals'
+import { Notes } from './Notes'
+import { Footer, AttachmentsFooter } from './Footer'
+import type {
+  TemplateConfig,
+  InvoiceData,
+  WorkshopInfo,
+  InvoiceSettingsProps,
+  PaymentSummary,
+  ImageAttachment,
+  OtherAttachment,
+} from './types'
+
+export function InvoicePDF({
+  data,
+  workshop,
+  invoiceSettings,
+  paymentSummary,
+  imageAttachments = [],
+  otherAttachments = [],
+  pdfAttachmentNames = [],
+  logoDataUri,
+  template,
+  torqvoiceLogoDataUri,
+}: {
+  data: InvoiceData
+  workshop?: WorkshopInfo
+  invoiceSettings?: InvoiceSettingsProps
+  paymentSummary?: PaymentSummary
+  imageAttachments?: ImageAttachment[]
+  otherAttachments?: OtherAttachment[]
+  pdfAttachmentNames?: string[]
+  logoDataUri?: string
+  template?: TemplateConfig
+  torqvoiceLogoDataUri?: string
+}) {
+  const primaryColor = template?.primaryColor || '#d97706'
+  const fontFamily = template?.fontFamily || 'Helvetica'
+  const showLogo = template?.showLogo !== false
+  const showCompanyName = template?.showCompanyName !== false
+  const headerStyle = template?.headerStyle || 'standard'
+  const styles = createStyles(primaryColor, fontFamily)
+  const fontBold = getFontBold(fontFamily)
+
+  const cc = invoiceSettings?.currencyCode || 'USD'
+  const vehicleName = `${data.vehicle.year} ${data.vehicle.make} ${data.vehicle.model}`
+  const partsSubtotal = data.partItems.reduce((sum, p) => sum + p.total, 0)
+  const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
+  const displayTotal = data.totalAmount > 0 ? data.totalAmount : data.cost
+  const invoiceNum = data.invoiceNumber || `INV-${data.id.slice(-8).toUpperCase()}`
+  const df = invoiceSettings?.dateFormat || DEFAULT_DATE_FORMAT
+  const tz = invoiceSettings?.timezone || undefined
+  const serviceDate = formatDateForPdf(data.serviceDate, df, tz)
+
+  const dueDays = invoiceSettings?.dueDays || 0
+  const dueDate =
+    dueDays > 0
+      ? formatDateForPdf(
+          new Date(new Date(data.serviceDate).getTime() + dueDays * 86400000),
+          df,
+          tz,
+        )
+      : null
+
+  const balanceDue = paymentSummary ? displayTotal - paymentSummary.totalPaid : displayTotal
+  const isPaidInFull = paymentSummary ? paymentSummary.totalPaid >= displayTotal : false
+
+  const shopDisplayName = workshop?.name || data.shopName || 'Torqvoice'
+  const hasAttachments = imageAttachments.length > 0 || otherAttachments.length > 0
+
+  return (
+    <Document>
+      <Page size="A4" style={styles.page}>
+        <Header
+          headerStyle={headerStyle}
+          primaryColor={primaryColor}
+          fontFamily={fontFamily}
+          showLogo={showLogo}
+          showCompanyName={showCompanyName}
+          logoDataUri={logoDataUri}
+          torqvoiceLogoDataUri={torqvoiceLogoDataUri}
+          workshop={workshop}
+          invoiceSettings={invoiceSettings}
+          shopDisplayName={shopDisplayName}
+          invoiceNum={invoiceNum}
+          serviceDate={serviceDate}
+          dueDate={dueDate}
+          styles={styles}
+        />
+
+        <InfoSection
+          data={data}
+          vehicleName={vehicleName}
+          invoiceSettings={invoiceSettings}
+          styles={styles}
+        />
+
+        <PartsTable data={data} currencyCode={cc} styles={styles} />
+        <LaborTable data={data} currencyCode={cc} styles={styles} />
+
+        {data.invoiceNotes && (
+          <View style={styles.notesSection}>
+            <Text style={styles.notesLabel}>Notes</Text>
+            <Text style={styles.notesText}>{data.invoiceNotes}</Text>
+          </View>
+        )}
+
+        <Totals
+          data={data}
+          currencyCode={cc}
+          primaryColor={primaryColor}
+          fontFamily={fontFamily}
+          displayTotal={displayTotal}
+          partsSubtotal={partsSubtotal}
+          laborSubtotal={laborSubtotal}
+          balanceDue={balanceDue}
+          isPaidInFull={isPaidInFull}
+          paymentSummary={paymentSummary}
+          styles={styles}
+        />
+
+        {torqvoiceLogoDataUri && (
+          <View
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              justifyContent: 'flex-start',
+              gap: 3,
+              marginTop: 6,
+            }}
+          >
+            <Text style={{ fontSize: 7, color: gray }}>Powered by</Text>
+            <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
+          </View>
+        )}
+
+        <Notes
+          invoiceNotes={null}
+          diagnosticNotes={data.diagnosticNotes}
+          invoiceSettings={invoiceSettings}
+          otherAttachments={otherAttachments}
+          pdfAttachmentNames={pdfAttachmentNames}
+          fontFamily={fontFamily}
+          styles={styles}
+        />
+
+        <Footer
+          shopDisplayName={shopDisplayName}
+          serviceDate={serviceDate}
+          invoiceSettings={invoiceSettings}
+          invoiceNum={invoiceNum}
+          primaryColor={primaryColor}
+          fontFamily={fontFamily}
+          torqvoiceLogoDataUri={torqvoiceLogoDataUri}
+          styles={styles}
+        />
+      </Page>
+
+      {hasAttachments && imageAttachments.length > 0 && (
+        <Page size="A4" style={styles.page}>
+          <Text style={styles.sectionTitle}>Diagnostic Report Images</Text>
+          <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
+            {imageAttachments.map((img, i) => (
+              <View key={i} style={{ width: '48%', marginBottom: 8 }}>
+                <Image
+                  src={img.dataUri}
+                  style={{
+                    maxHeight: 250,
+                    borderRadius: 4,
+                    objectFit: 'contain',
+                    objectPosition: 'left',
+                  }}
+                />
+                {img.description ? (
+                  <Text style={{ fontSize: 8, color: gray, marginTop: 2 }}>{img.description}</Text>
+                ) : (
+                  <Text style={styles.attachmentFileName}>{img.fileName}</Text>
+                )}
+              </View>
+            ))}
+          </View>
+          <AttachmentsFooter
+            shopDisplayName={shopDisplayName}
+            invoiceNum={invoiceNum}
+            styles={styles}
+          />
+        </Page>
+      )}
+    </Document>
+  )
+}

+ 69 - 0
src/features/vehicles/Components/invoice-pdf/Notes.tsx

@@ -0,0 +1,69 @@
+import { Text, View } from '@react-pdf/renderer'
+import type { InvoiceSettingsProps, OtherAttachment } from './types'
+import { getFontBold } from './styles'
+import type { Style } from '@react-pdf/types'
+
+interface NotesProps {
+  invoiceNotes: string | null
+  diagnosticNotes: string | null
+  invoiceSettings?: InvoiceSettingsProps
+  otherAttachments: OtherAttachment[]
+  pdfAttachmentNames: string[]
+  fontFamily: string
+  styles: Record<string, Style>
+}
+
+export function Notes({
+  invoiceNotes,
+  diagnosticNotes,
+  invoiceSettings,
+  otherAttachments,
+  pdfAttachmentNames,
+  fontFamily,
+  styles,
+}: NotesProps) {
+  const fontBold = getFontBold(fontFamily)
+
+  return (
+    <>
+      {invoiceNotes && (
+        <View style={styles.notesSection}>
+          <Text style={styles.notesLabel}>Notes</Text>
+          <Text style={styles.notesText}>{invoiceNotes}</Text>
+        </View>
+      )}
+
+      {invoiceSettings?.showBankAccount && invoiceSettings?.bankAccount && (
+        <View style={{ ...styles.notesSection, marginTop: 12 }}>
+          <Text style={styles.notesLabel}>Til Konto / Bank Account</Text>
+          <Text style={{ fontSize: 11, fontFamily: fontBold }}>
+            {invoiceSettings.bankAccount}
+          </Text>
+        </View>
+      )}
+
+      {diagnosticNotes && (
+        <View style={{ ...styles.notesSection, marginTop: 8 }}>
+          <Text style={styles.notesLabel}>Diagnostic Notes</Text>
+          <Text style={styles.notesText}>{diagnosticNotes}</Text>
+        </View>
+      )}
+
+      {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
+        <View style={{ ...styles.notesSection, marginTop: 8 }}>
+          <Text style={styles.notesLabel}>Attached Documents</Text>
+          {pdfAttachmentNames.map((name, i) => (
+            <Text key={`pdf-${i}`} style={styles.notesText}>
+              {name} (see appended pages)
+            </Text>
+          ))}
+          {otherAttachments.map((att, i) => (
+            <Text key={i} style={styles.notesText}>
+              {att.fileName}
+            </Text>
+          ))}
+        </View>
+      )}
+    </>
+  )
+}

+ 78 - 0
src/features/vehicles/Components/invoice-pdf/Tables.tsx

@@ -0,0 +1,78 @@
+import { Text, View } from '@react-pdf/renderer'
+import { formatCurrency } from '@/lib/format'
+import type { InvoiceData } from './types'
+import type { Style } from '@react-pdf/types'
+
+interface TablesProps {
+  data: InvoiceData
+  currencyCode: string
+  styles: Record<string, Style>
+}
+
+export function PartsTable({ data, currencyCode, styles }: TablesProps) {
+  if (data.partItems.length === 0) return null
+
+  return (
+    <View>
+      <Text style={styles.sectionTitle}>Parts</Text>
+      <View style={styles.table}>
+        <View style={styles.tableHeader}>
+          <Text style={{ ...styles.tableHeaderCell, width: '15%' }}>Part #</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '35%' }}>Description</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '12%', textAlign: 'right' }}>Qty</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '18%', textAlign: 'right' }}>
+            Unit Price
+          </Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>Total</Text>
+        </View>
+        {data.partItems.map((part, i) => (
+          <View key={i} style={styles.tableRow}>
+            <Text style={{ ...styles.tableCell, width: '15%' }}>{part.partNumber || '-'}</Text>
+            <Text style={{ ...styles.tableCell, width: '35%' }}>{part.name}</Text>
+            <Text style={{ ...styles.tableCell, width: '12%', textAlign: 'right' }}>
+              {part.quantity}
+            </Text>
+            <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right' }}>
+              {formatCurrency(part.unitPrice, currencyCode)}
+            </Text>
+            <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
+              {formatCurrency(part.total, currencyCode)}
+            </Text>
+          </View>
+        ))}
+      </View>
+    </View>
+  )
+}
+
+export function LaborTable({ data, currencyCode, styles }: TablesProps) {
+  if (data.laborItems.length === 0) return null
+
+  return (
+    <View>
+      <Text style={styles.sectionTitle}>Labor</Text>
+      <View style={styles.table}>
+        <View style={styles.tableHeader}>
+          <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>Description</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>Hours</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>Rate</Text>
+          <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>Total</Text>
+        </View>
+        {data.laborItems.map((labor, i) => (
+          <View key={i} style={styles.tableRow}>
+            <Text style={{ ...styles.tableCell, width: '45%' }}>{labor.description}</Text>
+            <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right' }}>
+              {labor.hours}
+            </Text>
+            <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right' }}>
+              {formatCurrency(labor.rate, currencyCode)}/hr
+            </Text>
+            <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
+              {formatCurrency(labor.total, currencyCode)}
+            </Text>
+          </View>
+        ))}
+      </View>
+    </View>
+  )
+}

+ 131 - 0
src/features/vehicles/Components/invoice-pdf/Totals.tsx

@@ -0,0 +1,131 @@
+import { Text, View } from '@react-pdf/renderer'
+import { formatCurrency } from '@/lib/format'
+import type { InvoiceData, PaymentSummary } from './types'
+import { gray, lightenColor, getFontBold } from './styles'
+import type { Style } from '@react-pdf/types'
+
+interface TotalsProps {
+  data: InvoiceData
+  currencyCode: string
+  primaryColor: string
+  fontFamily: string
+  displayTotal: number
+  partsSubtotal: number
+  laborSubtotal: number
+  balanceDue: number
+  isPaidInFull: boolean
+  paymentSummary?: PaymentSummary
+  styles: Record<string, Style>
+}
+
+export function Totals({
+  data,
+  currencyCode,
+  primaryColor,
+  fontFamily,
+  displayTotal,
+  partsSubtotal,
+  laborSubtotal,
+  balanceDue,
+  isPaidInFull,
+  paymentSummary,
+  styles,
+}: TotalsProps) {
+  const fontBold = getFontBold(fontFamily)
+
+  return (
+    <View style={styles.totalsBox}>
+      {data.partItems.length > 0 && (
+        <View style={styles.totalRow}>
+          <Text style={styles.totalLabel}>Parts</Text>
+          <Text style={styles.totalValue}>{formatCurrency(partsSubtotal, currencyCode)}</Text>
+        </View>
+      )}
+      {data.laborItems.length > 0 && (
+        <View style={styles.totalRow}>
+          <Text style={styles.totalLabel}>Labor</Text>
+          <Text style={styles.totalValue}>{formatCurrency(laborSubtotal, currencyCode)}</Text>
+        </View>
+      )}
+      {data.subtotal > 0 && (
+        <View style={styles.totalRow}>
+          <Text style={styles.totalLabel}>Subtotal</Text>
+          <Text style={styles.totalValue}>{formatCurrency(data.subtotal, currencyCode)}</Text>
+        </View>
+      )}
+      {(data.discountAmount ?? 0) > 0 && (
+        <View style={styles.totalRow}>
+          <Text style={styles.totalLabel}>
+            Discount{data.discountType === 'percentage' ? ` (${data.discountValue}%)` : ''}
+          </Text>
+          <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
+            {formatCurrency(-(data.discountAmount ?? 0), currencyCode)}
+          </Text>
+        </View>
+      )}
+      {data.taxRate > 0 && (
+        <View style={styles.totalRow}>
+          <Text style={styles.totalLabel}>Tax ({data.taxRate}%)</Text>
+          <Text style={styles.totalValue}>{formatCurrency(data.taxAmount, currencyCode)}</Text>
+        </View>
+      )}
+      <View style={styles.totalDivider} />
+      {paymentSummary && paymentSummary.payments.length > 0 ? (
+        <>
+          <View style={styles.totalRow}>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>Total</Text>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>
+              {formatCurrency(displayTotal, currencyCode)}
+            </Text>
+          </View>
+          <View style={{ ...styles.totalDivider, marginVertical: 6 }} />
+          <Text style={{ fontSize: 9, fontFamily: fontBold, marginBottom: 4 }}>
+            Payments Received
+          </Text>
+          {paymentSummary.payments.map((p, i) => (
+            <View key={i} style={styles.totalRow}>
+              <Text style={{ fontSize: 8, color: gray }}>
+                {p.date} ({p.method})
+              </Text>
+              <Text style={{ fontSize: 9, color: '#059669' }}>
+                {formatCurrency(-p.amount, currencyCode)}
+              </Text>
+            </View>
+          ))}
+          <View
+            style={{ borderTopWidth: 2, borderTopColor: primaryColor, marginVertical: 6 }}
+          />
+          <View
+            style={{
+              flexDirection: 'row',
+              justifyContent: 'space-between',
+              paddingVertical: 6,
+              backgroundColor: isPaidInFull ? '#f0fdf4' : lightenColor(primaryColor, 0.92),
+              marginHorizontal: -8,
+              paddingHorizontal: 8,
+              borderRadius: 3,
+            }}
+          >
+            <Text style={{ fontSize: 16, fontFamily: fontBold }}>
+              {isPaidInFull ? 'PAID IN FULL' : 'Amount Due'}
+            </Text>
+            <Text
+              style={{
+                fontSize: 16,
+                fontFamily: fontBold,
+                color: isPaidInFull ? '#059669' : primaryColor,
+              }}
+            >
+              {isPaidInFull ? '' : formatCurrency(balanceDue, currencyCode)}
+            </Text>
+          </View>
+        </>
+      ) : (
+        <View style={styles.grandTotalRow}>
+          <Text style={styles.grandTotalLabel}>Total</Text>
+          <Text style={styles.grandTotalValue}>{formatCurrency(displayTotal, currencyCode)}</Text>
+        </View>
+      )}
+    </View>
+  )
+}

+ 10 - 0
src/features/vehicles/Components/invoice-pdf/index.tsx

@@ -0,0 +1,10 @@
+export { InvoicePDF } from './InvoicePDF'
+export type {
+  TemplateConfig,
+  InvoiceData,
+  WorkshopInfo,
+  InvoiceSettingsProps,
+  PaymentSummary,
+  ImageAttachment,
+  OtherAttachment,
+} from './types'

+ 119 - 0
src/features/vehicles/Components/invoice-pdf/styles.ts

@@ -0,0 +1,119 @@
+import { StyleSheet } from '@react-pdf/renderer'
+
+export const gray = '#6b7280'
+export const grayLight = '#f3f4f6'
+export const dark = '#111827'
+
+export function hexToRgb(hex: string) {
+  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+  return result
+    ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }
+    : { r: 217, g: 119, b: 6 }
+}
+
+export function lightenColor(hex: string, factor: number = 0.9) {
+  const { r, g, b } = hexToRgb(hex)
+  return `rgb(${Math.round(r + (255 - r) * factor)}, ${Math.round(g + (255 - g) * factor)}, ${Math.round(b + (255 - b) * factor)})`
+}
+
+export function darkenColor(hex: string, factor: number = 0.3) {
+  const { r, g, b } = hexToRgb(hex)
+  return `rgb(${Math.round(r * (1 - factor))}, ${Math.round(g * (1 - factor))}, ${Math.round(b * (1 - factor))})`
+}
+
+export function getFontBold(font: string) {
+  const boldMap: Record<string, string> = {
+    'Times-Roman': 'Times-Bold',
+  }
+  return boldMap[font] || `${font}-Bold`
+}
+
+export function createStyles(primary: string, font: string) {
+  const primaryLight = lightenColor(primary)
+  const primaryDark = darkenColor(primary)
+  const fontBold = getFontBold(font)
+
+  return StyleSheet.create({
+    page: { padding: 40, fontSize: 10, fontFamily: font, color: dark },
+    header: {
+      flexDirection: 'row',
+      justifyContent: 'space-between',
+      marginBottom: 30,
+      paddingBottom: 15,
+      borderBottomWidth: 3,
+      borderBottomColor: primary,
+    },
+    brandName: { fontSize: 22, fontFamily: fontBold, color: primary },
+    brandSub: { fontSize: 9, color: gray, marginTop: 2 },
+    brandContact: { fontSize: 8, color: gray, marginTop: 1 },
+    invoiceTitle: { fontSize: 18, fontFamily: fontBold, textAlign: 'right' as const },
+    invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
+    infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
+    infoBox: { flex: 1, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
+    infoLabel: {
+      fontSize: 8,
+      fontFamily: fontBold,
+      color: primary,
+      textTransform: 'uppercase' as const,
+      marginBottom: 6,
+    },
+    infoText: { fontSize: 10, marginBottom: 2 },
+    infoTextBold: { fontSize: 10, fontFamily: fontBold, marginBottom: 2 },
+    infoTextSmall: { fontSize: 9, color: gray, marginBottom: 2 },
+    sectionTitle: {
+      fontSize: 12,
+      fontFamily: fontBold,
+      marginBottom: 8,
+      marginTop: 16,
+      color: dark,
+    },
+    table: { marginBottom: 4 },
+    tableHeader: {
+      flexDirection: 'row',
+      backgroundColor: primaryLight,
+      paddingVertical: 6,
+      paddingHorizontal: 8,
+      borderRadius: 2,
+    },
+    tableRow: {
+      flexDirection: 'row',
+      paddingVertical: 5,
+      paddingHorizontal: 8,
+      borderBottomWidth: 0.5,
+      borderBottomColor: '#e5e7eb',
+    },
+    tableCell: { fontSize: 9 },
+    tableCellBold: { fontSize: 9, fontFamily: fontBold },
+    tableHeaderCell: { fontSize: 8, fontFamily: fontBold, color: primaryDark },
+    totalsBox: { marginTop: 16, marginLeft: 'auto', width: 220 },
+    totalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
+    totalLabel: { fontSize: 10, color: gray },
+    totalValue: { fontSize: 10 },
+    totalDivider: { borderTopWidth: 1, borderTopColor: primary, marginVertical: 4 },
+    grandTotalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
+    grandTotalLabel: { fontSize: 14, fontFamily: fontBold },
+    grandTotalValue: { fontSize: 14, fontFamily: fontBold, color: primary },
+    notesSection: { marginTop: 20, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
+    notesLabel: {
+      fontSize: 8,
+      fontFamily: fontBold,
+      color: primary,
+      textTransform: 'uppercase' as const,
+      marginBottom: 4,
+    },
+    notesText: { fontSize: 9, color: gray, lineHeight: 1.5 },
+    attachmentFileName: { fontSize: 8, color: gray, marginTop: 4, marginBottom: 8 },
+    footer: {
+      position: 'absolute' as const,
+      bottom: 30,
+      left: 40,
+      right: 40,
+      textAlign: 'center' as const,
+      fontSize: 8,
+      color: gray,
+      paddingTop: 8,
+      borderTopWidth: 0.5,
+      borderTopColor: '#e5e7eb',
+    },
+  })
+}

+ 94 - 0
src/features/vehicles/Components/invoice-pdf/types.ts

@@ -0,0 +1,94 @@
+export interface TemplateConfig {
+  primaryColor?: string
+  fontFamily?: string
+  showLogo?: boolean
+  showCompanyName?: boolean
+  headerStyle?: string
+}
+
+export interface InvoiceData {
+  id: string
+  title: string
+  description: string | null
+  type: string
+  serviceDate: Date
+  shopName: string | null
+  techName: string | null
+  mileage: number | null
+  diagnosticNotes: string | null
+  invoiceNotes: string | null
+  subtotal: number
+  taxRate: number
+  taxAmount: number
+  totalAmount: number
+  cost: number
+  invoiceNumber: string | null
+  discountType?: string | null
+  discountValue?: number
+  discountAmount?: number
+  partItems: {
+    partNumber: string | null
+    name: string
+    quantity: number
+    unitPrice: number
+    total: number
+  }[]
+  laborItems: {
+    description: string
+    hours: number
+    rate: number
+    total: number
+  }[]
+  vehicle: {
+    make: string
+    model: string
+    year: number
+    vin: string | null
+    licensePlate: string | null
+    mileage: number
+    customer: {
+      name: string
+      email: string | null
+      phone: string | null
+      address: string | null
+      company: string | null
+    } | null
+  }
+}
+
+export interface WorkshopInfo {
+  name: string
+  address: string
+  phone: string
+  email: string
+}
+
+export interface InvoiceSettingsProps {
+  bankAccount?: string
+  orgNumber?: string
+  paymentTerms?: string
+  footerNote?: string
+  showBankAccount?: boolean
+  showOrgNumber?: boolean
+  dueDays?: number
+  currencyCode?: string
+  unitSystem?: string
+  dateFormat?: string
+  timezone?: string
+}
+
+export interface PaymentSummary {
+  totalPaid: number
+  payments: { amount: number; date: string; method: string }[]
+}
+
+export interface ImageAttachment {
+  fileName: string
+  dataUri: string
+  description?: string
+}
+
+export interface OtherAttachment {
+  fileName: string
+  fileType: string
+}