浏览代码

Add invoice/quote PDF layout editor with drag-and-drop section ordering (#51)

* layout

* new template custom fields options

* invoice layout

* layout

* templates

* pdf preview

* pdf preview and templates

* qoute digital layout

* layout

* translation

* lint

* translations
Bernt Christian Egeland 6 月之前
父节点
当前提交
5a6b7d04c4
共有 63 个文件被更改,包括 6435 次插入 和 1918 次删除
  1. 4 0
      messages/de/pdf.json
  2. 136 19
      messages/de/settings.json
  3. 4 0
      messages/en/pdf.json
  4. 3 1
      messages/en/service.json
  5. 75 6
      messages/en/settings.json
  6. 4 0
      messages/es/pdf.json
  7. 136 19
      messages/es/settings.json
  8. 4 0
      messages/fr/pdf.json
  9. 136 19
      messages/fr/settings.json
  10. 4 0
      messages/it/pdf.json
  11. 136 19
      messages/it/settings.json
  12. 4 0
      messages/nb/pdf.json
  13. 3 1
      messages/nb/service.json
  14. 75 6
      messages/nb/settings.json
  15. 4 0
      messages/nl/pdf.json
  16. 443 151
      messages/nl/settings.json
  17. 4 0
      messages/pl/pdf.json
  18. 215 48
      messages/pl/settings.json
  19. 4 0
      messages/pt-BR/pdf.json
  20. 136 19
      messages/pt-BR/settings.json
  21. 4 0
      messages/tr/pdf.json
  22. 136 19
      messages/tr/settings.json
  23. 15 0
      package-lock.json
  24. 1 0
      package.json
  25. 1 3
      src/__tests__/features/auth/passkey-signin.test.tsx
  26. 2 2
      src/__tests__/features/public/invoice-view.test.tsx
  27. 2 2
      src/__tests__/features/public/quote-view.test.tsx
  28. 302 147
      src/app/(authenticated)/settings/invoice/invoice-settings.tsx
  29. 29 2
      src/app/(authenticated)/settings/invoice/page.tsx
  30. 1 2
      src/app/(authenticated)/settings/settings-nav.tsx
  31. 10 2
      src/app/(authenticated)/settings/templates/page.tsx
  32. 92 231
      src/app/(authenticated)/settings/templates/template-settings.tsx
  33. 557 331
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  34. 22 0
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  35. 22 0
      src/app/(public)/share/quote/[orgId]/[token]/page.tsx
  36. 489 263
      src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx
  37. 23 0
      src/app/api/protected/quotes/[id]/pdf/route.ts
  38. 23 1
      src/app/api/protected/services/[id]/pdf/route.ts
  39. 23 1
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  40. 23 0
      src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts
  41. 1 1
      src/app/layout.tsx
  42. 2 2
      src/components/shared-link-card.tsx
  43. 49 9
      src/features/custom-fields/Components/CustomFieldsForm.tsx
  44. 226 151
      src/features/custom-fields/Components/CustomFieldsManager.tsx
  45. 440 262
      src/features/quotes/Components/QuotePDF.tsx
  46. 105 0
      src/features/settings/Actions/invoiceLayoutActions.ts
  47. 711 0
      src/features/settings/Components/InvoiceLayoutEditor.tsx
  48. 53 0
      src/features/settings/Components/InvoiceLayoutPreview.tsx
  49. 276 0
      src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx
  50. 490 0
      src/features/settings/Schema/invoiceLayoutSchema.ts
  51. 4 0
      src/features/settings/Schema/settingsSchema.ts
  52. 48 0
      src/features/vehicles/Components/invoice-pdf/CustomFields.tsx
  53. 0 8
      src/features/vehicles/Components/invoice-pdf/Footer.tsx
  54. 51 49
      src/features/vehicles/Components/invoice-pdf/Header.tsx
  55. 258 40
      src/features/vehicles/Components/invoice-pdf/InfoSection.tsx
  56. 190 54
      src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx
  57. 182 23
      src/features/vehicles/Components/invoice-pdf/Notes.tsx
  58. 1 1
      src/features/vehicles/Components/invoice-pdf/styles.ts
  59. 5 0
      src/features/vehicles/Components/invoice-pdf/types.ts
  60. 2 2
      src/features/vehicles/Components/service-detail/ServiceSidebar.tsx
  61. 7 0
      src/features/vehicles/Components/service-page/DetailsRightColumn.tsx
  62. 18 2
      src/features/vehicles/Components/service-page/useServiceActions.ts
  63. 9 0
      src/features/vehicles/Components/service-page/useServiceFormState.ts

+ 4 - 0
messages/de/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Fälliger Betrag",
     "amountDue": "Fälliger Betrag",
     "notes": "Hinweise",
     "notes": "Hinweise",
     "bankAccount": "Til Konto / Bankkonto",
     "bankAccount": "Til Konto / Bankkonto",
+    "paymentInformation": "Zahlungsinformationen",
+    "orgNumberLabel": "Org.-Nr.",
+    "paymentTermsLabel": "Zahlungsbedingungen",
+    "dueDateLabel": "Fälligkeitsdatum",
     "diagnosticNotes": "Diagnosehinweise",
     "diagnosticNotes": "Diagnosehinweise",
     "attachedDocuments": "Beigefügte Dokumente",
     "attachedDocuments": "Beigefügte Dokumente",
     "seeAppendedPages": "{name} (siehe angehängte Seiten)",
     "seeAppendedPages": "{name} (siehe angehängte Seiten)",

+ 136 - 19
messages/de/settings.json

@@ -10,24 +10,78 @@
       "system": "System"
       "system": "System"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Unternehmen", "description": "Firmendetails & Branding" },
-      "subscription": { "title": "Abonnement", "description": "Tarif & Abrechnung" },
-      "account": { "title": "Konto", "description": "Name, E-Mail & Passwort" },
-      "team": { "title": "Team", "description": "Mitglieder & Rollen" },
-      "invoice": { "title": "Rechnung", "description": "Rechnungslayout & Felder" },
-      "templates": { "title": "Vorlagen", "description": "Rechnungs-, Angebots- & Inspektionsvorlagen" },
-      "payment": { "title": "Zahlung", "description": "Zahlungsbedingungen & Konten" },
-      "currency": { "title": "Wahrung", "description": "Wahrungs- & Steuerstandards" },
-      "customFields": { "title": "Benutzerdefinierte Felder", "description": "Eigene Datenfelder definieren" },
-      "email": { "title": "E-Mail", "description": "E-Mail-Anbieter & Versand" },
-      "sms": { "title": "SMS", "description": "SMS-Anbieter & Nachrichten" },
-      "customerPortal": { "title": "Kundenportal", "description": "Kunden-Self-Service-Portal" },
-      "workshop": { "title": "Werkstatt", "description": "Techniker- & Arbeitsstandards" },
-      "maintenance": { "title": "Wartung", "description": "Vorhergesagte Serviceintervalle" },
-      "appearance": { "title": "Darstellung", "description": "Design & Anzeige" },
-      "data": { "title": "Daten", "description": "Export & Import-Backup" },
-      "license": { "title": "Lizenz", "description": "White-Label-Lizenz" },
-      "about": { "title": "Info", "description": "Version & Informationen" }
+      "company": {
+        "title": "Unternehmen",
+        "description": "Firmendetails & Branding"
+      },
+      "subscription": {
+        "title": "Abonnement",
+        "description": "Tarif & Abrechnung"
+      },
+      "account": {
+        "title": "Konto",
+        "description": "Name, E-Mail & Passwort"
+      },
+      "team": {
+        "title": "Team",
+        "description": "Mitglieder & Rollen"
+      },
+      "invoice": {
+        "title": "Rechnung & Angebote",
+        "description": "Rechnungseinstellungen, Layout & benutzerdefinierte Felder"
+      },
+      "templates": {
+        "title": "Vorlagen",
+        "description": "Visueller Stil, Farben & Schriftarten"
+      },
+      "payment": {
+        "title": "Zahlung",
+        "description": "Zahlungsbedingungen & Konten"
+      },
+      "currency": {
+        "title": "Wahrung",
+        "description": "Wahrungs- & Steuerstandards"
+      },
+      "customFields": {
+        "title": "Benutzerdefinierte Felder",
+        "description": "Eigene Datenfelder definieren"
+      },
+      "email": {
+        "title": "E-Mail",
+        "description": "E-Mail-Anbieter & Versand"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "SMS-Anbieter & Nachrichten"
+      },
+      "customerPortal": {
+        "title": "Kundenportal",
+        "description": "Kunden-Self-Service-Portal"
+      },
+      "workshop": {
+        "title": "Werkstatt",
+        "description": "Techniker- & Arbeitsstandards"
+      },
+      "maintenance": {
+        "title": "Wartung",
+        "description": "Vorhergesagte Serviceintervalle"
+      },
+      "appearance": {
+        "title": "Darstellung",
+        "description": "Design & Anzeige"
+      },
+      "data": {
+        "title": "Daten",
+        "description": "Export & Import-Backup"
+      },
+      "license": {
+        "title": "Lizenz",
+        "description": "White-Label-Lizenz"
+      },
+      "about": {
+        "title": "Info",
+        "description": "Version & Informationen"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Rechnungslayout",
     "title": "Rechnungslayout",
     "description": "Konfigurieren Sie, was auf generierten PDF-Rechnungen erscheint.",
     "description": "Konfigurieren Sie, was auf generierten PDF-Rechnungen erscheint.",
+    "tabs": {
+      "general": "Allgemein",
+      "layout": "Layout",
+      "customFields": "Benutzerdefinierte Felder"
+    },
     "invoiceNumberFormat": "Rechnungsnummernformat",
     "invoiceNumberFormat": "Rechnungsnummernformat",
     "invoiceNumberFormatHint": "Verwenden Sie <code>{year}</code> fur das aktuelle Jahr. Vorschau: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Verwenden Sie <code>{year}</code> fur das aktuelle Jahr. Vorschau: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Nachste Rechnungsnummer",
     "nextInvoiceNumber": "Nachste Rechnungsnummer",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Vielen Dank fur Ihren Auftrag!",
     "footerPlaceholder": "Vielen Dank fur Ihren Auftrag!",
     "footerHint": "Dieser Text erscheint am Ende jeder Rechnung",
     "footerHint": "Dieser Text erscheint am Ende jeder Rechnung",
     "saveInvoice": "Rechnungseinstellungen speichern",
     "saveInvoice": "Rechnungseinstellungen speichern",
-    "saved": "Rechnungseinstellungen gespeichert"
+    "saved": "Rechnungseinstellungen gespeichert",
+    "layoutDescription": "Passen Sie die Reihenfolge und Sichtbarkeit der Abschnitte in Ihren Rechnungen und Angeboten an.",
+    "layoutSaved": "Layout erfolgreich gespeichert",
+    "preview": "Vorschau",
+    "layoutDocInvoice": "Rechnung",
+    "layoutDocQuote": "Angebot",
+    "saveLayout": "Layout speichern",
+    "resetLayout": "Zurücksetzen"
+  },
+  "layoutEditor": {
+    "helpText": "Ziehen zum Neuordnen. Verwenden Sie den Breiten-Umschalter, um Abschnitte nebeneinander zu platzieren. Aufeinanderfolgende Halbbreite-Abschnitte (L/R) werden in zwei Spalten dargestellt.",
+    "addField": "Feld hinzufügen",
+    "custom": "benutzerdefiniert",
+    "full": "Voll",
+    "L": "L",
+    "R": "R",
+    "left": "links",
+    "right": "rechts",
+    "halfWidthHint": "Halbbreite ({column}) — Klicken für volle Breite",
+    "fullWidthHint": "Volle Breite — Klicken für Halbbreite (links)",
+    "sections": {
+      "header": "Kopfzeile",
+      "customer": "Kunde",
+      "vehicle": "Fahrzeug",
+      "service": "Service",
+      "parts_table": "Teile",
+      "labor_table": "Arbeit",
+      "totals": "Summen",
+      "notes": "Notizen",
+      "diagnostic_notes": "Diagnosenotizen",
+      "bank_account": "Bankkonto",
+      "footer": "Fußzeile",
+      "general": "Allgemein"
+    },
+    "fields": {
+      "customer_name": "Kundenname",
+      "customer_company": "Firma",
+      "customer_address": "Adresse",
+      "customer_email": "E-Mail",
+      "customer_phone": "Telefon",
+      "vehicle_name": "Fahrzeug",
+      "vin": "FIN",
+      "license_plate": "Kennzeichen",
+      "mileage": "Kilometerstand",
+      "service_title": "Servicetitel",
+      "service_type": "Servicetyp",
+      "tech_name": "Techniker",
+      "logo": "Logo",
+      "company_name": "Firmenname",
+      "bank_account": "Bankkonto",
+      "org_number": "Organisationsnummer"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Vorlagen",
     "title": "Vorlagen",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Kopfzeilenstil",
     "headerStyle": "Kopfzeilenstil",
+    "logoSize": "Logogröße",
     "standard": "Standard",
     "standard": "Standard",
     "compact": "Kompakt",
     "compact": "Kompakt",
     "modern": "Modern",
     "modern": "Modern",
@@ -315,6 +426,12 @@
     "rate": "Satz",
     "rate": "Satz",
     "subtotal": "Zwischensumme",
     "subtotal": "Zwischensumme",
     "tax": "Steuer ({rate}%)",
     "tax": "Steuer ({rate}%)",
+    "previewService": "Service",
+    "previewNotes": "Anmerkungen",
+    "previewDiagnosticNotes": "Diagnosebericht",
+    "previewDue": "Fällig: {date}",
+    "previewAdditionalInfo": "Zusätzliche Informationen",
+    "previewThankYou": "Vielen Dank für Ihren Auftrag",
     "saveInvoiceTemplate": "Rechnungsvorlage speichern",
     "saveInvoiceTemplate": "Rechnungsvorlage speichern",
     "saveQuotationTemplate": "Angebotsvorlage speichern",
     "saveQuotationTemplate": "Angebotsvorlage speichern",
     "invoiceTemplateSaved": "Rechnungsvorlageneinstellungen gespeichert",
     "invoiceTemplateSaved": "Rechnungsvorlageneinstellungen gespeichert",

+ 4 - 0
messages/en/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Amount Due",
     "amountDue": "Amount Due",
     "notes": "Notes",
     "notes": "Notes",
     "bankAccount": "Til Konto / Bank Account",
     "bankAccount": "Til Konto / Bank Account",
+    "paymentInformation": "Payment Information",
+    "orgNumberLabel": "Org. Number",
+    "paymentTermsLabel": "Payment Terms",
+    "dueDateLabel": "Due Date",
     "diagnosticNotes": "Diagnostic Notes",
     "diagnosticNotes": "Diagnostic Notes",
     "attachedDocuments": "Attached Documents",
     "attachedDocuments": "Attached Documents",
     "seeAppendedPages": "{name} (see appended pages)",
     "seeAppendedPages": "{name} (see appended pages)",

+ 3 - 1
messages/en/service.json

@@ -6,7 +6,9 @@
     "deleteTitle": "Delete Service Record",
     "deleteTitle": "Delete Service Record",
     "deleteDescription": "This will permanently delete this service record and all associated data. This cannot be undone.",
     "deleteDescription": "This will permanently delete this service record and all associated data. This cannot be undone.",
     "deleted": "Service record deleted",
     "deleted": "Service record deleted",
-    "failedDelete": "Failed to delete"
+    "failedDelete": "Failed to delete",
+    "customFieldsInvalid": "Please fill in all required custom fields",
+    "customFieldsSaveFailed": "Failed to save custom fields"
   },
   },
   "header": {
   "header": {
     "tabs": {
     "tabs": {

+ 75 - 6
messages/en/settings.json

@@ -14,8 +14,8 @@
       "subscription": { "title": "Subscription", "description": "Plan & billing" },
       "subscription": { "title": "Subscription", "description": "Plan & billing" },
       "account": { "title": "Account", "description": "Name, email & password" },
       "account": { "title": "Account", "description": "Name, email & password" },
       "team": { "title": "Team", "description": "Members & roles" },
       "team": { "title": "Team", "description": "Members & roles" },
-      "invoice": { "title": "Invoice", "description": "Invoice layout & fields" },
-      "templates": { "title": "Templates", "description": "Invoice, quote & inspection templates" },
+      "invoice": { "title": "Invoice & Quotes", "description": "Invoice settings, layout & custom fields" },
+      "templates": { "title": "Templates", "description": "Visual style, colors & fonts" },
       "payment": { "title": "Payment", "description": "Payment terms & accounts" },
       "payment": { "title": "Payment", "description": "Payment terms & accounts" },
       "currency": { "title": "Currency", "description": "Currency & tax defaults" },
       "currency": { "title": "Currency", "description": "Currency & tax defaults" },
       "customFields": { "title": "Custom Fields", "description": "Define custom data fields" },
       "customFields": { "title": "Custom Fields", "description": "Define custom data fields" },
@@ -256,8 +256,13 @@
     "failedUpdateRole": "Failed to update role"
     "failedUpdateRole": "Failed to update role"
   },
   },
   "invoice": {
   "invoice": {
-    "title": "Invoice Layout",
-    "description": "Configure what appears on generated PDF invoices.",
+    "title": "Invoice & Quotes",
+    "description": "Configure invoice settings, layout and custom fields.",
+    "tabs": {
+      "general": "General",
+      "layout": "Layout",
+      "customFields": "Custom Fields"
+    },
     "invoiceNumberFormat": "Invoice Number Format",
     "invoiceNumberFormat": "Invoice Number Format",
     "invoiceNumberFormatHint": "Use <code>{year}</code> for the current year. Preview: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Use <code>{year}</code> for the current year. Preview: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Next Invoice Number",
     "nextInvoiceNumber": "Next Invoice Number",
@@ -278,7 +283,58 @@
     "footerPlaceholder": "Thank you for your business!",
     "footerPlaceholder": "Thank you for your business!",
     "footerHint": "This text appears at the bottom of every invoice",
     "footerHint": "This text appears at the bottom of every invoice",
     "saveInvoice": "Save Invoice Settings",
     "saveInvoice": "Save Invoice Settings",
-    "saved": "Invoice settings saved"
+    "saved": "Invoice settings saved",
+    "layoutDescription": "Customize the order and visibility of sections in your invoices and quotes.",
+    "layoutSaved": "Layout saved successfully",
+    "preview": "Preview",
+    "layoutDocInvoice": "Invoice",
+    "layoutDocQuote": "Quote",
+    "saveLayout": "Save Layout",
+    "resetLayout": "Reset to Default"
+  },
+  "layoutEditor": {
+    "helpText": "Drag to reorder. Use the width toggle to place sections side-by-side. Consecutive half-width sections (L/R) will render in two columns.",
+    "addField": "Add field",
+    "custom": "custom",
+    "full": "Full",
+    "L": "L",
+    "R": "R",
+    "left": "left",
+    "right": "right",
+    "halfWidthHint": "Half width ({column}) — click to make full width",
+    "fullWidthHint": "Full width — click to make half width (left)",
+    "sections": {
+      "header": "Header",
+      "customer": "Customer",
+      "vehicle": "Vehicle",
+      "service": "Service",
+      "parts_table": "Parts Table",
+      "labor_table": "Labor Table",
+      "totals": "Totals",
+      "notes": "Notes",
+      "diagnostic_notes": "Diagnostic Notes",
+      "bank_account": "Bank Account",
+      "footer": "Footer",
+      "general": "General"
+    },
+    "fields": {
+      "customer_name": "Customer Name",
+      "customer_company": "Customer Company",
+      "customer_address": "Customer Address",
+      "customer_email": "Customer Email",
+      "customer_phone": "Customer Phone",
+      "vehicle_name": "Vehicle",
+      "vin": "VIN",
+      "license_plate": "License Plate",
+      "mileage": "Mileage",
+      "service_title": "Service Title",
+      "service_type": "Service Type",
+      "tech_name": "Technician",
+      "logo": "Logo",
+      "company_name": "Company Name",
+      "bank_account": "Bank Account",
+      "org_number": "Organization Number"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Templates",
     "title": "Templates",
@@ -289,7 +345,8 @@
       "invoice": "Invoice",
       "invoice": "Invoice",
       "quotation": "Quotation",
       "quotation": "Quotation",
       "inspections": "Inspections",
       "inspections": "Inspections",
-      "sms": "SMS"
+      "sms": "SMS",
+      "layout": "Layout"
     },
     },
     "presets": "Template Presets",
     "presets": "Template Presets",
     "primaryColor": "Primary Color",
     "primaryColor": "Primary Color",
@@ -299,6 +356,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Header Style",
     "headerStyle": "Header Style",
+    "logoSize": "Logo Size",
     "standard": "Standard",
     "standard": "Standard",
     "compact": "Compact",
     "compact": "Compact",
     "modern": "Modern",
     "modern": "Modern",
@@ -315,6 +373,12 @@
     "rate": "Rate",
     "rate": "Rate",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
     "tax": "Tax ({rate}%)",
     "tax": "Tax ({rate}%)",
+    "previewService": "Service",
+    "previewNotes": "Notes",
+    "previewDiagnosticNotes": "Diagnostic Notes",
+    "previewDue": "Due: {date}",
+    "previewAdditionalInfo": "Additional Information",
+    "previewThankYou": "Thank you for your business",
     "saveInvoiceTemplate": "Save Invoice Template",
     "saveInvoiceTemplate": "Save Invoice Template",
     "saveQuotationTemplate": "Save Quotation Template",
     "saveQuotationTemplate": "Save Quotation Template",
     "invoiceTemplateSaved": "Invoice template settings saved",
     "invoiceTemplateSaved": "Invoice template settings saved",
@@ -341,6 +405,11 @@
     "paymentReceivedDescription": "Sent when a payment is recorded for an invoice.",
     "paymentReceivedDescription": "Sent when a payment is recorded for an invoice.",
     "saveSmsTemplates": "Save SMS Templates",
     "saveSmsTemplates": "Save SMS Templates",
     "smsTemplateSaved": "SMS template settings saved",
     "smsTemplateSaved": "SMS template settings saved",
+    "layoutTitle": "Invoice Layout",
+    "layoutDescription": "Customize the order and visibility of sections in your invoices and quotes.",
+    "layoutSaved": "Layout saved successfully",
+    "layoutDocInvoice": "Invoice",
+    "layoutDocQuote": "Quote",
     "smsDefaults": {
     "smsDefaults": {
       "invoiceReady": "Your invoice is ready. View it here: {share_link}",
       "invoiceReady": "Your invoice is ready. View it here: {share_link}",
       "quoteReady": "Your quote is ready for review. View it here: {share_link}",
       "quoteReady": "Your quote is ready for review. View it here: {share_link}",

+ 4 - 0
messages/es/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Monto adeudado",
     "amountDue": "Monto adeudado",
     "notes": "Notas",
     "notes": "Notas",
     "bankAccount": "Til Konto / Cuenta bancaria",
     "bankAccount": "Til Konto / Cuenta bancaria",
+    "paymentInformation": "Información de pago",
+    "orgNumberLabel": "Núm. de org.",
+    "paymentTermsLabel": "Condiciones de pago",
+    "dueDateLabel": "Fecha de vencimiento",
     "diagnosticNotes": "Notas de diagnóstico",
     "diagnosticNotes": "Notas de diagnóstico",
     "attachedDocuments": "Documentos adjuntos",
     "attachedDocuments": "Documentos adjuntos",
     "seeAppendedPages": "{name} (ver páginas adjuntas)",
     "seeAppendedPages": "{name} (ver páginas adjuntas)",

+ 136 - 19
messages/es/settings.json

@@ -10,24 +10,78 @@
       "system": "Sistema"
       "system": "Sistema"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Empresa", "description": "Datos comerciales y marca" },
-      "subscription": { "title": "Suscripcion", "description": "Plan y facturacion" },
-      "account": { "title": "Cuenta", "description": "Nombre, correo y contrasena" },
-      "team": { "title": "Equipo", "description": "Miembros y roles" },
-      "invoice": { "title": "Factura", "description": "Diseno y campos de factura" },
-      "templates": { "title": "Plantillas", "description": "Plantillas de facturas, cotizaciones e inspecciones" },
-      "payment": { "title": "Pago", "description": "Condiciones de pago y cuentas" },
-      "currency": { "title": "Moneda", "description": "Valores predeterminados de moneda e impuestos" },
-      "customFields": { "title": "Campos personalizados", "description": "Definir campos de datos personalizados" },
-      "email": { "title": "Correo", "description": "Proveedor de correo y envio" },
-      "sms": { "title": "SMS", "description": "Proveedor de SMS y mensajeria" },
-      "customerPortal": { "title": "Portal del cliente", "description": "Portal de autoservicio del cliente" },
-      "workshop": { "title": "Taller", "description": "Valores predeterminados de tecnico y mano de obra" },
-      "maintenance": { "title": "Mantenimiento", "description": "Intervalos de servicio predichos" },
-      "appearance": { "title": "Apariencia", "description": "Tema y visualizacion" },
-      "data": { "title": "Datos", "description": "Exportar e importar respaldo" },
-      "license": { "title": "Licencia", "description": "Licencia marca blanca" },
-      "about": { "title": "Acerca de", "description": "Version e informacion" }
+      "company": {
+        "title": "Empresa",
+        "description": "Datos comerciales y marca"
+      },
+      "subscription": {
+        "title": "Suscripcion",
+        "description": "Plan y facturacion"
+      },
+      "account": {
+        "title": "Cuenta",
+        "description": "Nombre, correo y contrasena"
+      },
+      "team": {
+        "title": "Equipo",
+        "description": "Miembros y roles"
+      },
+      "invoice": {
+        "title": "Facturas y presupuestos",
+        "description": "Configuracion de facturas, diseno y campos personalizados"
+      },
+      "templates": {
+        "title": "Plantillas",
+        "description": "Estilo visual, colores y fuentes"
+      },
+      "payment": {
+        "title": "Pago",
+        "description": "Condiciones de pago y cuentas"
+      },
+      "currency": {
+        "title": "Moneda",
+        "description": "Valores predeterminados de moneda e impuestos"
+      },
+      "customFields": {
+        "title": "Campos personalizados",
+        "description": "Definir campos de datos personalizados"
+      },
+      "email": {
+        "title": "Correo",
+        "description": "Proveedor de correo y envio"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Proveedor de SMS y mensajeria"
+      },
+      "customerPortal": {
+        "title": "Portal del cliente",
+        "description": "Portal de autoservicio del cliente"
+      },
+      "workshop": {
+        "title": "Taller",
+        "description": "Valores predeterminados de tecnico y mano de obra"
+      },
+      "maintenance": {
+        "title": "Mantenimiento",
+        "description": "Intervalos de servicio predichos"
+      },
+      "appearance": {
+        "title": "Apariencia",
+        "description": "Tema y visualizacion"
+      },
+      "data": {
+        "title": "Datos",
+        "description": "Exportar e importar respaldo"
+      },
+      "license": {
+        "title": "Licencia",
+        "description": "Licencia marca blanca"
+      },
+      "about": {
+        "title": "Acerca de",
+        "description": "Version e informacion"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Diseno de factura",
     "title": "Diseno de factura",
     "description": "Configure lo que aparece en las facturas PDF generadas.",
     "description": "Configure lo que aparece en las facturas PDF generadas.",
+    "tabs": {
+      "general": "General",
+      "layout": "Diseño",
+      "customFields": "Campos personalizados"
+    },
     "invoiceNumberFormat": "Formato de numero de factura",
     "invoiceNumberFormat": "Formato de numero de factura",
     "invoiceNumberFormatHint": "Use <code>{year}</code> para el ano actual. Vista previa: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Use <code>{year}</code> para el ano actual. Vista previa: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Siguiente numero de factura",
     "nextInvoiceNumber": "Siguiente numero de factura",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Gracias por su preferencia!",
     "footerPlaceholder": "Gracias por su preferencia!",
     "footerHint": "Este texto aparece al final de cada factura",
     "footerHint": "Este texto aparece al final de cada factura",
     "saveInvoice": "Guardar configuracion de factura",
     "saveInvoice": "Guardar configuracion de factura",
-    "saved": "Configuracion de factura guardada"
+    "saved": "Configuracion de factura guardada",
+    "layoutDescription": "Personaliza el orden y la visibilidad de las secciones en tus facturas y presupuestos.",
+    "layoutSaved": "Diseño guardado correctamente",
+    "preview": "Vista previa",
+    "layoutDocInvoice": "Factura",
+    "layoutDocQuote": "Presupuesto",
+    "saveLayout": "Guardar diseño",
+    "resetLayout": "Restablecer"
+  },
+  "layoutEditor": {
+    "helpText": "Arrastra para reordenar. Usa el selector de ancho para colocar secciones lado a lado. Las secciones consecutivas de medio ancho (I/D) se mostrarán en dos columnas.",
+    "addField": "Añadir campo",
+    "custom": "personalizado",
+    "full": "Completo",
+    "L": "I",
+    "R": "D",
+    "left": "izquierda",
+    "right": "derecha",
+    "halfWidthHint": "Medio ancho ({column}) — clic para ancho completo",
+    "fullWidthHint": "Ancho completo — clic para medio ancho (izquierda)",
+    "sections": {
+      "header": "Encabezado",
+      "customer": "Cliente",
+      "vehicle": "Vehículo",
+      "service": "Servicio",
+      "parts_table": "Piezas",
+      "labor_table": "Mano de obra",
+      "totals": "Totales",
+      "notes": "Notas",
+      "diagnostic_notes": "Notas de diagnóstico",
+      "bank_account": "Cuenta bancaria",
+      "footer": "Pie de página",
+      "general": "General"
+    },
+    "fields": {
+      "customer_name": "Nombre del cliente",
+      "customer_company": "Empresa",
+      "customer_address": "Dirección",
+      "customer_email": "Correo electrónico",
+      "customer_phone": "Teléfono",
+      "vehicle_name": "Vehículo",
+      "vin": "VIN",
+      "license_plate": "Matrícula",
+      "mileage": "Kilometraje",
+      "service_title": "Título del servicio",
+      "service_type": "Tipo de servicio",
+      "tech_name": "Técnico",
+      "logo": "Logo",
+      "company_name": "Nombre de empresa",
+      "bank_account": "Cuenta bancaria",
+      "org_number": "Número de organización"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Plantillas",
     "title": "Plantillas",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Estilo de encabezado",
     "headerStyle": "Estilo de encabezado",
+    "logoSize": "Tamaño del logotipo",
     "standard": "Estandar",
     "standard": "Estandar",
     "compact": "Compacto",
     "compact": "Compacto",
     "modern": "Moderno",
     "modern": "Moderno",
@@ -315,6 +426,12 @@
     "rate": "Tarifa",
     "rate": "Tarifa",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
     "tax": "Impuesto ({rate}%)",
     "tax": "Impuesto ({rate}%)",
+    "previewService": "Servicio",
+    "previewNotes": "Notas",
+    "previewDiagnosticNotes": "Notas de diagnóstico",
+    "previewDue": "Vencimiento: {date}",
+    "previewAdditionalInfo": "Información adicional",
+    "previewThankYou": "Gracias por su confianza",
     "saveInvoiceTemplate": "Guardar plantilla de factura",
     "saveInvoiceTemplate": "Guardar plantilla de factura",
     "saveQuotationTemplate": "Guardar plantilla de cotizacion",
     "saveQuotationTemplate": "Guardar plantilla de cotizacion",
     "invoiceTemplateSaved": "Configuracion de plantilla de factura guardada",
     "invoiceTemplateSaved": "Configuracion de plantilla de factura guardada",

+ 4 - 0
messages/fr/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Montant dû",
     "amountDue": "Montant dû",
     "notes": "Notes",
     "notes": "Notes",
     "bankAccount": "Til Konto / Compte bancaire",
     "bankAccount": "Til Konto / Compte bancaire",
+    "paymentInformation": "Informations de paiement",
+    "orgNumberLabel": "N° d'org.",
+    "paymentTermsLabel": "Conditions de paiement",
+    "dueDateLabel": "Date d'échéance",
     "diagnosticNotes": "Notes de diagnostic",
     "diagnosticNotes": "Notes de diagnostic",
     "attachedDocuments": "Documents joints",
     "attachedDocuments": "Documents joints",
     "seeAppendedPages": "{name} (voir pages annexées)",
     "seeAppendedPages": "{name} (voir pages annexées)",

+ 136 - 19
messages/fr/settings.json

@@ -10,24 +10,78 @@
       "system": "Systeme"
       "system": "Systeme"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Entreprise", "description": "Details commerciaux et image de marque" },
-      "subscription": { "title": "Abonnement", "description": "Plan et facturation" },
-      "account": { "title": "Compte", "description": "Nom, e-mail et mot de passe" },
-      "team": { "title": "Equipe", "description": "Membres et roles" },
-      "invoice": { "title": "Facture", "description": "Mise en page et champs de facture" },
-      "templates": { "title": "Modeles", "description": "Modeles de factures, devis et inspections" },
-      "payment": { "title": "Paiement", "description": "Conditions de paiement et comptes" },
-      "currency": { "title": "Devise", "description": "Devise et taxes par defaut" },
-      "customFields": { "title": "Champs personnalises", "description": "Definir des champs de donnees personnalises" },
-      "email": { "title": "E-mail", "description": "Fournisseur et envoi d'e-mails" },
-      "sms": { "title": "SMS", "description": "Fournisseur SMS et messagerie" },
-      "customerPortal": { "title": "Portail client", "description": "Portail libre-service client" },
-      "workshop": { "title": "Atelier", "description": "Valeurs par defaut technicien et main-d'oeuvre" },
-      "maintenance": { "title": "Maintenance", "description": "Intervalles de service predits" },
-      "appearance": { "title": "Apparence", "description": "Theme et affichage" },
-      "data": { "title": "Donnees", "description": "Exporter et importer une sauvegarde" },
-      "license": { "title": "Licence", "description": "Licence marque blanche" },
-      "about": { "title": "A propos", "description": "Version et informations" }
+      "company": {
+        "title": "Entreprise",
+        "description": "Details commerciaux et image de marque"
+      },
+      "subscription": {
+        "title": "Abonnement",
+        "description": "Plan et facturation"
+      },
+      "account": {
+        "title": "Compte",
+        "description": "Nom, e-mail et mot de passe"
+      },
+      "team": {
+        "title": "Equipe",
+        "description": "Membres et roles"
+      },
+      "invoice": {
+        "title": "Factures et devis",
+        "description": "Parametres de facturation, mise en page et champs personnalises"
+      },
+      "templates": {
+        "title": "Modeles",
+        "description": "Style visuel, couleurs et polices"
+      },
+      "payment": {
+        "title": "Paiement",
+        "description": "Conditions de paiement et comptes"
+      },
+      "currency": {
+        "title": "Devise",
+        "description": "Devise et taxes par defaut"
+      },
+      "customFields": {
+        "title": "Champs personnalises",
+        "description": "Definir des champs de donnees personnalises"
+      },
+      "email": {
+        "title": "E-mail",
+        "description": "Fournisseur et envoi d'e-mails"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Fournisseur SMS et messagerie"
+      },
+      "customerPortal": {
+        "title": "Portail client",
+        "description": "Portail libre-service client"
+      },
+      "workshop": {
+        "title": "Atelier",
+        "description": "Valeurs par defaut technicien et main-d'oeuvre"
+      },
+      "maintenance": {
+        "title": "Maintenance",
+        "description": "Intervalles de service predits"
+      },
+      "appearance": {
+        "title": "Apparence",
+        "description": "Theme et affichage"
+      },
+      "data": {
+        "title": "Donnees",
+        "description": "Exporter et importer une sauvegarde"
+      },
+      "license": {
+        "title": "Licence",
+        "description": "Licence marque blanche"
+      },
+      "about": {
+        "title": "A propos",
+        "description": "Version et informations"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Mise en page de facture",
     "title": "Mise en page de facture",
     "description": "Configurez ce qui apparait sur les factures PDF generees.",
     "description": "Configurez ce qui apparait sur les factures PDF generees.",
+    "tabs": {
+      "general": "Général",
+      "layout": "Mise en page",
+      "customFields": "Champs personnalisés"
+    },
     "invoiceNumberFormat": "Format du numero de facture",
     "invoiceNumberFormat": "Format du numero de facture",
     "invoiceNumberFormatHint": "Utilisez <code>{year}</code> pour l'annee en cours. Apercu : <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Utilisez <code>{year}</code> pour l'annee en cours. Apercu : <bold>{preview}</bold>",
     "nextInvoiceNumber": "Prochain numero de facture",
     "nextInvoiceNumber": "Prochain numero de facture",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Merci pour votre confiance !",
     "footerPlaceholder": "Merci pour votre confiance !",
     "footerHint": "Ce texte apparait en bas de chaque facture",
     "footerHint": "Ce texte apparait en bas de chaque facture",
     "saveInvoice": "Enregistrer les parametres de facture",
     "saveInvoice": "Enregistrer les parametres de facture",
-    "saved": "Parametres de facture enregistres"
+    "saved": "Parametres de facture enregistres",
+    "layoutDescription": "Personnalisez l'ordre et la visibilité des sections dans vos factures et devis.",
+    "layoutSaved": "Mise en page enregistrée",
+    "preview": "Aperçu",
+    "layoutDocInvoice": "Facture",
+    "layoutDocQuote": "Devis",
+    "saveLayout": "Enregistrer la mise en page",
+    "resetLayout": "Réinitialiser"
+  },
+  "layoutEditor": {
+    "helpText": "Glissez pour réorganiser. Utilisez le sélecteur de largeur pour placer les sections côte à côte. Les sections consécutives en demi-largeur (G/D) s'afficheront en deux colonnes.",
+    "addField": "Ajouter un champ",
+    "custom": "personnalisé",
+    "full": "Plein",
+    "L": "G",
+    "R": "D",
+    "left": "gauche",
+    "right": "droite",
+    "halfWidthHint": "Demi-largeur ({column}) — cliquer pour pleine largeur",
+    "fullWidthHint": "Pleine largeur — cliquer pour demi-largeur (gauche)",
+    "sections": {
+      "header": "En-tête",
+      "customer": "Client",
+      "vehicle": "Véhicule",
+      "service": "Service",
+      "parts_table": "Pièces",
+      "labor_table": "Main-d'œuvre",
+      "totals": "Totaux",
+      "notes": "Notes",
+      "diagnostic_notes": "Notes de diagnostic",
+      "bank_account": "Compte bancaire",
+      "footer": "Pied de page",
+      "general": "Général"
+    },
+    "fields": {
+      "customer_name": "Nom du client",
+      "customer_company": "Entreprise",
+      "customer_address": "Adresse",
+      "customer_email": "E-mail",
+      "customer_phone": "Téléphone",
+      "vehicle_name": "Véhicule",
+      "vin": "VIN",
+      "license_plate": "Plaque d'immatriculation",
+      "mileage": "Kilométrage",
+      "service_title": "Titre du service",
+      "service_type": "Type de service",
+      "tech_name": "Technicien",
+      "logo": "Logo",
+      "company_name": "Nom de l'entreprise",
+      "bank_account": "Compte bancaire",
+      "org_number": "Numéro d'organisation"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Modeles",
     "title": "Modeles",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Style d'en-tete",
     "headerStyle": "Style d'en-tete",
+    "logoSize": "Taille du logo",
     "standard": "Standard",
     "standard": "Standard",
     "compact": "Compact",
     "compact": "Compact",
     "modern": "Moderne",
     "modern": "Moderne",
@@ -315,6 +426,12 @@
     "rate": "Taux",
     "rate": "Taux",
     "subtotal": "Sous-total",
     "subtotal": "Sous-total",
     "tax": "Taxe ({rate}%)",
     "tax": "Taxe ({rate}%)",
+    "previewService": "Service",
+    "previewNotes": "Notes",
+    "previewDiagnosticNotes": "Notes de diagnostic",
+    "previewDue": "Échéance: {date}",
+    "previewAdditionalInfo": "Informations supplémentaires",
+    "previewThankYou": "Merci pour votre confiance",
     "saveInvoiceTemplate": "Enregistrer le modele de facture",
     "saveInvoiceTemplate": "Enregistrer le modele de facture",
     "saveQuotationTemplate": "Enregistrer le modele de devis",
     "saveQuotationTemplate": "Enregistrer le modele de devis",
     "invoiceTemplateSaved": "Parametres du modele de facture enregistres",
     "invoiceTemplateSaved": "Parametres du modele de facture enregistres",

+ 4 - 0
messages/it/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Importo Dovuto",
     "amountDue": "Importo Dovuto",
     "notes": "Note",
     "notes": "Note",
     "bankAccount": "Til Konto / Conto Bancario",
     "bankAccount": "Til Konto / Conto Bancario",
+    "paymentInformation": "Informazioni di pagamento",
+    "orgNumberLabel": "N. org.",
+    "paymentTermsLabel": "Condizioni di pagamento",
+    "dueDateLabel": "Data di scadenza",
     "diagnosticNotes": "Note Diagnostiche",
     "diagnosticNotes": "Note Diagnostiche",
     "attachedDocuments": "Documenti Allegati",
     "attachedDocuments": "Documenti Allegati",
     "seeAppendedPages": "{name} (vedi pagine allegate)",
     "seeAppendedPages": "{name} (vedi pagine allegate)",

+ 136 - 19
messages/it/settings.json

@@ -10,24 +10,78 @@
       "system": "Sistema"
       "system": "Sistema"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Azienda", "description": "Dettagli aziendali e branding" },
-      "subscription": { "title": "Abbonamento", "description": "Piano e fatturazione" },
-      "account": { "title": "Account", "description": "Nome, email e password" },
-      "team": { "title": "Team", "description": "Membri e ruoli" },
-      "invoice": { "title": "Fattura", "description": "Layout e campi fattura" },
-      "templates": { "title": "Modelli", "description": "Modelli fattura, preventivo e ispezione" },
-      "payment": { "title": "Pagamento", "description": "Termini di pagamento e conti" },
-      "currency": { "title": "Valuta", "description": "Valuta e imposte predefinite" },
-      "customFields": { "title": "Campi personalizzati", "description": "Definisci campi dati personalizzati" },
-      "email": { "title": "Email", "description": "Provider email e invio" },
-      "sms": { "title": "SMS", "description": "Provider SMS e messaggistica" },
-      "customerPortal": { "title": "Portale clienti", "description": "Portale self-service clienti" },
-      "workshop": { "title": "Officina", "description": "Tecnico e valori predefiniti manodopera" },
-      "maintenance": { "title": "Manutenzione", "description": "Intervalli di servizio previsti" },
-      "appearance": { "title": "Aspetto", "description": "Tema e visualizzazione" },
-      "data": { "title": "Dati", "description": "Esportazione e importazione backup" },
-      "license": { "title": "Licenza", "description": "Licenza white-label" },
-      "about": { "title": "Informazioni", "description": "Versione e info" }
+      "company": {
+        "title": "Azienda",
+        "description": "Dettagli aziendali e branding"
+      },
+      "subscription": {
+        "title": "Abbonamento",
+        "description": "Piano e fatturazione"
+      },
+      "account": {
+        "title": "Account",
+        "description": "Nome, email e password"
+      },
+      "team": {
+        "title": "Team",
+        "description": "Membri e ruoli"
+      },
+      "invoice": {
+        "title": "Fatture e preventivi",
+        "description": "Impostazioni fattura, layout e campi personalizzati"
+      },
+      "templates": {
+        "title": "Modelli",
+        "description": "Stile visivo, colori e caratteri"
+      },
+      "payment": {
+        "title": "Pagamento",
+        "description": "Termini di pagamento e conti"
+      },
+      "currency": {
+        "title": "Valuta",
+        "description": "Valuta e imposte predefinite"
+      },
+      "customFields": {
+        "title": "Campi personalizzati",
+        "description": "Definisci campi dati personalizzati"
+      },
+      "email": {
+        "title": "Email",
+        "description": "Provider email e invio"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Provider SMS e messaggistica"
+      },
+      "customerPortal": {
+        "title": "Portale clienti",
+        "description": "Portale self-service clienti"
+      },
+      "workshop": {
+        "title": "Officina",
+        "description": "Tecnico e valori predefiniti manodopera"
+      },
+      "maintenance": {
+        "title": "Manutenzione",
+        "description": "Intervalli di servizio previsti"
+      },
+      "appearance": {
+        "title": "Aspetto",
+        "description": "Tema e visualizzazione"
+      },
+      "data": {
+        "title": "Dati",
+        "description": "Esportazione e importazione backup"
+      },
+      "license": {
+        "title": "Licenza",
+        "description": "Licenza white-label"
+      },
+      "about": {
+        "title": "Informazioni",
+        "description": "Versione e info"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Layout fattura",
     "title": "Layout fattura",
     "description": "Configura cosa appare sulle fatture PDF generate.",
     "description": "Configura cosa appare sulle fatture PDF generate.",
+    "tabs": {
+      "general": "Generale",
+      "layout": "Layout",
+      "customFields": "Campi personalizzati"
+    },
     "invoiceNumberFormat": "Formato numero fattura",
     "invoiceNumberFormat": "Formato numero fattura",
     "invoiceNumberFormatHint": "Usa <code>{year}</code> per l'anno corrente. Anteprima: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Usa <code>{year}</code> per l'anno corrente. Anteprima: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Prossimo numero fattura",
     "nextInvoiceNumber": "Prossimo numero fattura",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Grazie per la vostra fiducia!",
     "footerPlaceholder": "Grazie per la vostra fiducia!",
     "footerHint": "Questo testo appare in fondo a ogni fattura",
     "footerHint": "Questo testo appare in fondo a ogni fattura",
     "saveInvoice": "Salva impostazioni fattura",
     "saveInvoice": "Salva impostazioni fattura",
-    "saved": "Impostazioni fattura salvate"
+    "saved": "Impostazioni fattura salvate",
+    "layoutDescription": "Personalizza l'ordine e la visibilità delle sezioni nelle tue fatture e preventivi.",
+    "layoutSaved": "Layout salvato con successo",
+    "preview": "Anteprima",
+    "layoutDocInvoice": "Fattura",
+    "layoutDocQuote": "Preventivo",
+    "saveLayout": "Salva layout",
+    "resetLayout": "Ripristina"
+  },
+  "layoutEditor": {
+    "helpText": "Trascina per riordinare. Usa il selettore di larghezza per posizionare le sezioni affiancate. Le sezioni consecutive a mezza larghezza (S/D) verranno visualizzate in due colonne.",
+    "addField": "Aggiungi campo",
+    "custom": "personalizzato",
+    "full": "Intero",
+    "L": "S",
+    "R": "D",
+    "left": "sinistra",
+    "right": "destra",
+    "halfWidthHint": "Mezza larghezza ({column}) — clicca per larghezza intera",
+    "fullWidthHint": "Larghezza intera — clicca per mezza larghezza (sinistra)",
+    "sections": {
+      "header": "Intestazione",
+      "customer": "Cliente",
+      "vehicle": "Veicolo",
+      "service": "Servizio",
+      "parts_table": "Ricambi",
+      "labor_table": "Manodopera",
+      "totals": "Totali",
+      "notes": "Note",
+      "diagnostic_notes": "Note diagnostiche",
+      "bank_account": "Conto bancario",
+      "footer": "Piè di pagina",
+      "general": "Generale"
+    },
+    "fields": {
+      "customer_name": "Nome cliente",
+      "customer_company": "Azienda",
+      "customer_address": "Indirizzo",
+      "customer_email": "E-mail",
+      "customer_phone": "Telefono",
+      "vehicle_name": "Veicolo",
+      "vin": "VIN",
+      "license_plate": "Targa",
+      "mileage": "Chilometraggio",
+      "service_title": "Titolo servizio",
+      "service_type": "Tipo di servizio",
+      "tech_name": "Tecnico",
+      "logo": "Logo",
+      "company_name": "Nome azienda",
+      "bank_account": "Conto bancario",
+      "org_number": "Numero organizzazione"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Modelli",
     "title": "Modelli",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Stile intestazione",
     "headerStyle": "Stile intestazione",
+    "logoSize": "Dimensione logo",
     "standard": "Standard",
     "standard": "Standard",
     "compact": "Compatto",
     "compact": "Compatto",
     "modern": "Moderno",
     "modern": "Moderno",
@@ -315,6 +426,12 @@
     "rate": "Tariffa",
     "rate": "Tariffa",
     "subtotal": "Subtotale",
     "subtotal": "Subtotale",
     "tax": "IVA ({rate}%)",
     "tax": "IVA ({rate}%)",
+    "previewService": "Servizio",
+    "previewNotes": "Note",
+    "previewDiagnosticNotes": "Note diagnostiche",
+    "previewDue": "Scadenza: {date}",
+    "previewAdditionalInfo": "Informazioni aggiuntive",
+    "previewThankYou": "Grazie per la vostra fiducia",
     "saveInvoiceTemplate": "Salva modello fattura",
     "saveInvoiceTemplate": "Salva modello fattura",
     "saveQuotationTemplate": "Salva modello preventivo",
     "saveQuotationTemplate": "Salva modello preventivo",
     "invoiceTemplateSaved": "Impostazioni modello fattura salvate",
     "invoiceTemplateSaved": "Impostazioni modello fattura salvate",

+ 4 - 0
messages/nb/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Beløp å betale",
     "amountDue": "Beløp å betale",
     "notes": "Notater",
     "notes": "Notater",
     "bankAccount": "Til Konto",
     "bankAccount": "Til Konto",
+    "paymentInformation": "Betalingsinformasjon",
+    "orgNumberLabel": "Org.nummer",
+    "paymentTermsLabel": "Betalingsbetingelser",
+    "dueDateLabel": "Forfallsdato",
     "diagnosticNotes": "Diagnosenotater",
     "diagnosticNotes": "Diagnosenotater",
     "attachedDocuments": "Vedlagte dokumenter",
     "attachedDocuments": "Vedlagte dokumenter",
     "seeAppendedPages": "{name} (se vedlagte sider)",
     "seeAppendedPages": "{name} (se vedlagte sider)",

+ 3 - 1
messages/nb/service.json

@@ -6,7 +6,9 @@
     "deleteTitle": "Slett servicepost",
     "deleteTitle": "Slett servicepost",
     "deleteDescription": "Dette vil permanent slette denne servicepotten og all tilknyttet data. Dette kan ikke angres.",
     "deleteDescription": "Dette vil permanent slette denne servicepotten og all tilknyttet data. Dette kan ikke angres.",
     "deleted": "Servicepost slettet",
     "deleted": "Servicepost slettet",
-    "failedDelete": "Kunne ikke slette"
+    "failedDelete": "Kunne ikke slette",
+    "customFieldsInvalid": "Vennligst fyll ut alle obligatoriske egendefinerte felt",
+    "customFieldsSaveFailed": "Kunne ikke lagre egendefinerte felt"
   },
   },
   "header": {
   "header": {
     "tabs": {
     "tabs": {

+ 75 - 6
messages/nb/settings.json

@@ -14,8 +14,8 @@
       "subscription": { "title": "Abonnement", "description": "Plan og fakturering" },
       "subscription": { "title": "Abonnement", "description": "Plan og fakturering" },
       "account": { "title": "Konto", "description": "Navn, e-post og passord" },
       "account": { "title": "Konto", "description": "Navn, e-post og passord" },
       "team": { "title": "Team", "description": "Medlemmer og roller" },
       "team": { "title": "Team", "description": "Medlemmer og roller" },
-      "invoice": { "title": "Faktura", "description": "Fakturalayout og felter" },
-      "templates": { "title": "Maler", "description": "Faktura-, tilbuds- og inspeksjonsmaler" },
+      "invoice": { "title": "Faktura og tilbud", "description": "Fakturainnstillinger, layout og egendefinerte felter" },
+      "templates": { "title": "Maler", "description": "Visuell stil, farger og skrifter" },
       "payment": { "title": "Betaling", "description": "Betalingsvilkar og kontoer" },
       "payment": { "title": "Betaling", "description": "Betalingsvilkar og kontoer" },
       "currency": { "title": "Valuta", "description": "Valuta- og avgiftsstandarder" },
       "currency": { "title": "Valuta", "description": "Valuta- og avgiftsstandarder" },
       "customFields": { "title": "Egendefinerte felter", "description": "Definer egne datafelter" },
       "customFields": { "title": "Egendefinerte felter", "description": "Definer egne datafelter" },
@@ -256,8 +256,13 @@
     "failedUpdateRole": "Kunne ikke oppdatere rolle"
     "failedUpdateRole": "Kunne ikke oppdatere rolle"
   },
   },
   "invoice": {
   "invoice": {
-    "title": "Fakturalayout",
-    "description": "Konfigurer hva som vises pa genererte PDF-fakturaer.",
+    "title": "Faktura og tilbud",
+    "description": "Konfigurer fakturainnstillinger, layout og egendefinerte felter.",
+    "tabs": {
+      "general": "Generelt",
+      "layout": "Oppsett",
+      "customFields": "Egendefinerte felter"
+    },
     "invoiceNumberFormat": "Fakturanummerformat",
     "invoiceNumberFormat": "Fakturanummerformat",
     "invoiceNumberFormatHint": "Bruk <code>{year}</code> for innevarende ar. Forhandsvisning: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Bruk <code>{year}</code> for innevarende ar. Forhandsvisning: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Neste fakturanummer",
     "nextInvoiceNumber": "Neste fakturanummer",
@@ -278,7 +283,58 @@
     "footerPlaceholder": "Takk for handelen!",
     "footerPlaceholder": "Takk for handelen!",
     "footerHint": "Denne teksten vises nederst pa hver faktura",
     "footerHint": "Denne teksten vises nederst pa hver faktura",
     "saveInvoice": "Lagre fakturainnstillinger",
     "saveInvoice": "Lagre fakturainnstillinger",
-    "saved": "Fakturainnstillinger lagret"
+    "saved": "Fakturainnstillinger lagret",
+    "layoutDescription": "Tilpass rekkefølgen og synligheten av seksjoner i fakturaer og tilbud.",
+    "layoutSaved": "Oppsett lagret",
+    "preview": "Forhåndsvisning",
+    "layoutDocInvoice": "Faktura",
+    "layoutDocQuote": "Tilbud",
+    "saveLayout": "Lagre oppsett",
+    "resetLayout": "Tilbakestill"
+  },
+  "layoutEditor": {
+    "helpText": "Dra for å sortere. Bruk bredde-veksler for å plassere seksjoner side om side. Påfølgende halvbredde-seksjoner (V/H) vises i to kolonner.",
+    "addField": "Legg til felt",
+    "custom": "egendefinert",
+    "full": "Full",
+    "L": "V",
+    "R": "H",
+    "left": "venstre",
+    "right": "høyre",
+    "halfWidthHint": "Halvbredde ({column}) — klikk for full bredde",
+    "fullWidthHint": "Full bredde — klikk for halvbredde (venstre)",
+    "sections": {
+      "header": "Topptekst",
+      "customer": "Kunde",
+      "vehicle": "Kjøretøy",
+      "service": "Tjeneste",
+      "parts_table": "Deler",
+      "labor_table": "Arbeid",
+      "totals": "Totaler",
+      "notes": "Notater",
+      "diagnostic_notes": "Diagnostiske notater",
+      "bank_account": "Bankkonto",
+      "footer": "Bunntekst",
+      "general": "Generelt"
+    },
+    "fields": {
+      "customer_name": "Kundenavn",
+      "customer_company": "Firma",
+      "customer_address": "Adresse",
+      "customer_email": "E-post",
+      "customer_phone": "Telefon",
+      "vehicle_name": "Kjøretøy",
+      "vin": "VIN",
+      "license_plate": "Reg.nr",
+      "mileage": "Kilometerstand",
+      "service_title": "Tjenestetittel",
+      "service_type": "Tjenestetype",
+      "tech_name": "Tekniker",
+      "logo": "Logo",
+      "company_name": "Firmanavn",
+      "bank_account": "Bankkonto",
+      "org_number": "Organisasjonsnummer"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Maler",
     "title": "Maler",
@@ -289,7 +345,8 @@
       "invoice": "Faktura",
       "invoice": "Faktura",
       "quotation": "Tilbud",
       "quotation": "Tilbud",
       "inspections": "Inspeksjoner",
       "inspections": "Inspeksjoner",
-      "sms": "SMS"
+      "sms": "SMS",
+      "layout": "Oppsett"
     },
     },
     "presets": "Malforandsvisninger",
     "presets": "Malforandsvisninger",
     "primaryColor": "Primarfarge",
     "primaryColor": "Primarfarge",
@@ -299,6 +356,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Toppstil",
     "headerStyle": "Toppstil",
+    "logoSize": "Logostørrelse",
     "standard": "Standard",
     "standard": "Standard",
     "compact": "Kompakt",
     "compact": "Kompakt",
     "modern": "Moderne",
     "modern": "Moderne",
@@ -315,6 +373,12 @@
     "rate": "Sats",
     "rate": "Sats",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
     "tax": "Avgift ({rate}%)",
     "tax": "Avgift ({rate}%)",
+    "previewService": "Tjeneste",
+    "previewNotes": "Notater",
+    "previewDiagnosticNotes": "Diagnostiske notater",
+    "previewDue": "Forfaller: {date}",
+    "previewAdditionalInfo": "Tilleggsinformasjon",
+    "previewThankYou": "Takk for at du valgte oss",
     "saveInvoiceTemplate": "Lagre fakturamal",
     "saveInvoiceTemplate": "Lagre fakturamal",
     "saveQuotationTemplate": "Lagre tilbudsmal",
     "saveQuotationTemplate": "Lagre tilbudsmal",
     "invoiceTemplateSaved": "Fakturamalinnstillinger lagret",
     "invoiceTemplateSaved": "Fakturamalinnstillinger lagret",
@@ -341,6 +405,11 @@
     "paymentReceivedDescription": "Sendes nar en betaling registreres for en faktura.",
     "paymentReceivedDescription": "Sendes nar en betaling registreres for en faktura.",
     "saveSmsTemplates": "Lagre SMS-maler",
     "saveSmsTemplates": "Lagre SMS-maler",
     "smsTemplateSaved": "SMS-malinnstillinger lagret",
     "smsTemplateSaved": "SMS-malinnstillinger lagret",
+    "layoutTitle": "Fakturaoppsett",
+    "layoutDescription": "Tilpass rekkefølgen og synligheten av seksjoner i fakturaer og tilbud.",
+    "layoutSaved": "Oppsett lagret",
+    "layoutDocInvoice": "Faktura",
+    "layoutDocQuote": "Tilbud",
     "smsDefaults": {
     "smsDefaults": {
       "invoiceReady": "Fakturaen din er klar. Se den her: {share_link}",
       "invoiceReady": "Fakturaen din er klar. Se den her: {share_link}",
       "quoteReady": "Tilbudet ditt er klart for gjennomgang. Se det her: {share_link}",
       "quoteReady": "Tilbudet ditt er klart for gjennomgang. Se det her: {share_link}",

+ 4 - 0
messages/nl/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Te betalen bedrag",
     "amountDue": "Te betalen bedrag",
     "notes": "Notities",
     "notes": "Notities",
     "bankAccount": "Til Konto / Bankrekening",
     "bankAccount": "Til Konto / Bankrekening",
+    "paymentInformation": "Betalingsinformatie",
+    "orgNumberLabel": "Org.nr.",
+    "paymentTermsLabel": "Betalingsvoorwaarden",
+    "dueDateLabel": "Vervaldatum",
     "diagnosticNotes": "Diagnosenotities",
     "diagnosticNotes": "Diagnosenotities",
     "attachedDocuments": "Bijgevoegde documenten",
     "attachedDocuments": "Bijgevoegde documenten",
     "seeAppendedPages": "{name} (zie bijgevoegde pagina's)",
     "seeAppendedPages": "{name} (zie bijgevoegde pagina's)",

+ 443 - 151
messages/nl/settings.json

@@ -10,24 +10,78 @@
       "system": "Systeem"
       "system": "Systeem"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Bedrijf", "description": "Bedrijfsgegevens en branding" },
-      "subscription": { "title": "Abonnement", "description": "Plan en facturatie" },
-      "account": { "title": "Account", "description": "Naam, e-mail en wachtwoord" },
-      "team": { "title": "Team", "description": "Leden en rollen" },
-      "invoice": { "title": "Factuur", "description": "Factuurindeling en velden" },
-      "templates": { "title": "Sjablonen", "description": "Factuur-, offerte- en inspectiesjablonen" },
-      "payment": { "title": "Betaling", "description": "Betalingsvoorwaarden en rekeningen" },
-      "currency": { "title": "Valuta", "description": "Standaardvaluta en belasting" },
-      "customFields": { "title": "Aangepaste velden", "description": "Eigen gegevensvelden definiëren" },
-      "email": { "title": "E-mail", "description": "E-mailprovider en verzending" },
-      "sms": { "title": "SMS", "description": "SMS-provider en berichten" },
-      "customerPortal": { "title": "Klantenportaal", "description": "Selfservice klantenportaal" },
-      "workshop": { "title": "Werkplaats", "description": "Standaard technicus en arbeid" },
-      "maintenance": { "title": "Onderhoud", "description": "Voorspelde service-intervallen" },
-      "appearance": { "title": "Weergave", "description": "Thema en weergave" },
-      "data": { "title": "Gegevens", "description": "Exporteer en importeer back-up" },
-      "license": { "title": "Licentie", "description": "White-label licentie" },
-      "about": { "title": "Over", "description": "Versie en informatie" }
+      "company": {
+        "title": "Bedrijf",
+        "description": "Bedrijfsgegevens en branding"
+      },
+      "subscription": {
+        "title": "Abonnement",
+        "description": "Plan en facturatie"
+      },
+      "account": {
+        "title": "Account",
+        "description": "Naam, e-mail en wachtwoord"
+      },
+      "team": {
+        "title": "Team",
+        "description": "Leden en rollen"
+      },
+      "invoice": {
+        "title": "Facturen en offertes",
+        "description": "Factuurinstellingen, indeling en aangepaste velden"
+      },
+      "templates": {
+        "title": "Sjablonen",
+        "description": "Visuele stijl, kleuren en lettertypen"
+      },
+      "payment": {
+        "title": "Betaling",
+        "description": "Betalingsvoorwaarden en rekeningen"
+      },
+      "currency": {
+        "title": "Valuta",
+        "description": "Standaardvaluta en belasting"
+      },
+      "customFields": {
+        "title": "Aangepaste velden",
+        "description": "Eigen gegevensvelden definiëren"
+      },
+      "email": {
+        "title": "E-mail",
+        "description": "E-mailprovider en verzending"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "SMS-provider en berichten"
+      },
+      "customerPortal": {
+        "title": "Klantenportaal",
+        "description": "Selfservice klantenportaal"
+      },
+      "workshop": {
+        "title": "Werkplaats",
+        "description": "Standaard technicus en arbeid"
+      },
+      "maintenance": {
+        "title": "Onderhoud",
+        "description": "Voorspelde service-intervallen"
+      },
+      "appearance": {
+        "title": "Weergave",
+        "description": "Thema en weergave"
+      },
+      "data": {
+        "title": "Gegevens",
+        "description": "Exporteer en importeer back-up"
+      },
+      "license": {
+        "title": "Licentie",
+        "description": "White-label licentie"
+      },
+      "about": {
+        "title": "Over",
+        "description": "Versie en informatie"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -40,21 +94,31 @@
     "activateLicense": "Licentie activeren"
     "activateLicense": "Licentie activeren"
   },
   },
   "company": {
   "company": {
-    "logoTitle": "Bedrijfslogo", "logoAlt": "Bedrijfslogo",
-    "upload": "Uploaden", "remove": "Verwijderen",
+    "logoTitle": "Bedrijfslogo",
+    "logoAlt": "Bedrijfslogo",
+    "upload": "Uploaden",
+    "remove": "Verwijderen",
     "logoHint": "JPG, PNG, WebP, SVG — max 2MB",
     "logoHint": "JPG, PNG, WebP, SVG — max 2MB",
-    "uploadingLogo": "Logo uploaden...", "logoUpdated": "Logo bijgewerkt",
+    "uploadingLogo": "Logo uploaden...",
+    "logoUpdated": "Logo bijgewerkt",
     "failedUploadLogo": "Logo uploaden mislukt",
     "failedUploadLogo": "Logo uploaden mislukt",
     "detailsTitle": "Bedrijfsgegevens",
     "detailsTitle": "Bedrijfsgegevens",
-    "shopName": "Bedrijfs-/werkplaatsnaam", "shopNamePlaceholder": "Jansens Autogarage",
-    "orgNumber": "Organisatienummer", "orgNumberPlaceholder": "123 456 789",
-    "phone": "Telefoon", "phonePlaceholder": "+31 20 123 4567",
-    "email": "E-mail", "emailPlaceholder": "service@voorbeeld.nl",
-    "address": "Adres", "addressPlaceholder": "Hoofdstraat 123\n1000 AB Amsterdam",
+    "shopName": "Bedrijfs-/werkplaatsnaam",
+    "shopNamePlaceholder": "Jansens Autogarage",
+    "orgNumber": "Organisatienummer",
+    "orgNumberPlaceholder": "123 456 789",
+    "phone": "Telefoon",
+    "phonePlaceholder": "+31 20 123 4567",
+    "email": "E-mail",
+    "emailPlaceholder": "service@voorbeeld.nl",
+    "address": "Adres",
+    "addressPlaceholder": "Hoofdstraat 123\n1000 AB Amsterdam",
     "saveCompany": "Bedrijfsgegevens opslaan",
     "saveCompany": "Bedrijfsgegevens opslaan",
     "addNewCompany": "Nieuw bedrijf toevoegen",
     "addNewCompany": "Nieuw bedrijf toevoegen",
-    "companyName": "Bedrijfsnaam", "companyNamePlaceholder": "Bijv. Jansens Autogarage",
-    "cancel": "Annuleren", "createCompany": "Bedrijf aanmaken",
+    "companyName": "Bedrijfsnaam",
+    "companyNamePlaceholder": "Bijv. Jansens Autogarage",
+    "cancel": "Annuleren",
+    "createCompany": "Bedrijf aanmaken",
     "settingsSaved": "Bedrijfsinstellingen opgeslagen",
     "settingsSaved": "Bedrijfsinstellingen opgeslagen",
     "failedSave": "Instellingen opslaan mislukt",
     "failedSave": "Instellingen opslaan mislukt",
     "companyCreated": "Bedrijf succesvol aangemaakt"
     "companyCreated": "Bedrijf succesvol aangemaakt"
@@ -69,25 +133,35 @@
     "verificationEmailFailed": "Kon verificatie-e-mail niet verzenden",
     "verificationEmailFailed": "Kon verificatie-e-mail niet verzenden",
     "resendCooldown": "Opnieuw verzenden over {seconds}s",
     "resendCooldown": "Opnieuw verzenden over {seconds}s",
     "emailChangeSent": "Er is een bevestigingslink naar uw nieuwe e-mailadres gestuurd. Uw e-mail wordt bijgewerkt zodra u op de link klikt.",
     "emailChangeSent": "Er is een bevestigingslink naar uw nieuwe e-mailadres gestuurd. Uw e-mail wordt bijgewerkt zodra u op de link klikt.",
-    "name": "Naam", "namePlaceholder": "Uw naam",
-    "emailLabel": "E-mail", "emailPlaceholder": "Uw e-mail",
-    "saveProfile": "Profiel opslaan", "profileUpdated": "Profiel bijgewerkt",
-    "failedUpdateProfile": "Profiel bijwerken mislukt", "failedUpdateEmail": "E-mail bijwerken mislukt",
+    "name": "Naam",
+    "namePlaceholder": "Uw naam",
+    "emailLabel": "E-mail",
+    "emailPlaceholder": "Uw e-mail",
+    "saveProfile": "Profiel opslaan",
+    "profileUpdated": "Profiel bijgewerkt",
+    "failedUpdateProfile": "Profiel bijwerken mislukt",
+    "failedUpdateEmail": "E-mail bijwerken mislukt",
     "changePasswordTitle": "Wachtwoord wijzigen",
     "changePasswordTitle": "Wachtwoord wijzigen",
     "changePasswordDescription": "Voer uw huidige wachtwoord in en kies een nieuw wachtwoord.",
     "changePasswordDescription": "Voer uw huidige wachtwoord in en kies een nieuw wachtwoord.",
-    "currentPassword": "Huidig wachtwoord", "newPassword": "Nieuw wachtwoord",
-    "confirmNewPassword": "Bevestig nieuw wachtwoord", "changePassword": "Wachtwoord wijzigen",
-    "passwordChanged": "Wachtwoord gewijzigd", "passwordsNoMatch": "Wachtwoorden komen niet overeen",
+    "currentPassword": "Huidig wachtwoord",
+    "newPassword": "Nieuw wachtwoord",
+    "confirmNewPassword": "Bevestig nieuw wachtwoord",
+    "changePassword": "Wachtwoord wijzigen",
+    "passwordChanged": "Wachtwoord gewijzigd",
+    "passwordsNoMatch": "Wachtwoorden komen niet overeen",
     "passwordMinLength": "Wachtwoord moet minimaal 8 tekens bevatten",
     "passwordMinLength": "Wachtwoord moet minimaal 8 tekens bevatten",
     "failedChangePassword": "Wachtwoord wijzigen mislukt",
     "failedChangePassword": "Wachtwoord wijzigen mislukt",
     "twoFactorTitle": "Tweefactorauthenticatie",
     "twoFactorTitle": "Tweefactorauthenticatie",
     "twoFactorEnabled": "Tweefactorauthenticatie is ingeschakeld",
     "twoFactorEnabled": "Tweefactorauthenticatie is ingeschakeld",
     "twoFactorDisablePrompt": "Voer uw wachtwoord in om tweefactorauthenticatie uit te schakelen.",
     "twoFactorDisablePrompt": "Voer uw wachtwoord in om tweefactorauthenticatie uit te schakelen.",
     "twoFactorEnablePrompt": "Voeg een extra beveiligingslaag toe aan uw account door tweefactorauthenticatie in te schakelen met een authenticatie-app.",
     "twoFactorEnablePrompt": "Voeg een extra beveiligingslaag toe aan uw account door tweefactorauthenticatie in te schakelen met een authenticatie-app.",
-    "password": "Wachtwoord", "enterPassword": "Voer uw wachtwoord in",
-    "disable2FA": "2FA uitschakelen", "enable2FA": "2FA inschakelen",
+    "password": "Wachtwoord",
+    "enterPassword": "Voer uw wachtwoord in",
+    "disable2FA": "2FA uitschakelen",
+    "enable2FA": "2FA inschakelen",
     "twoFactorDisabled": "Tweefactorauthenticatie uitgeschakeld",
     "twoFactorDisabled": "Tweefactorauthenticatie uitgeschakeld",
-    "failedEnable2FA": "2FA inschakelen mislukt", "failedDisable2FA": "2FA uitschakelen mislukt",
+    "failedEnable2FA": "2FA inschakelen mislukt",
+    "failedDisable2FA": "2FA uitschakelen mislukt",
     "enterPasswordError": "Voer uw wachtwoord in",
     "enterPasswordError": "Voer uw wachtwoord in",
     "setupDialogTitle": "Tweefactorauthenticatie inschakelen",
     "setupDialogTitle": "Tweefactorauthenticatie inschakelen",
     "setupDialogDescription": "Voer uw wachtwoord in om te beginnen met het instellen van 2FA met een authenticatie-app.",
     "setupDialogDescription": "Voer uw wachtwoord in om te beginnen met het instellen van 2FA met een authenticatie-app.",
@@ -96,13 +170,17 @@
     "scanQRDescription": "Scan deze QR-code met uw authenticatie-app (Google Authenticator, Authy, etc.)",
     "scanQRDescription": "Scan deze QR-code met uw authenticatie-app (Google Authenticator, Authy, etc.)",
     "verifyCodeTitle": "Code verifiëren",
     "verifyCodeTitle": "Code verifiëren",
     "verifyCodeDescription": "Voer de 6-cijferige code uit uw authenticatie-app in om te verifiëren dat deze correct is ingesteld.",
     "verifyCodeDescription": "Voer de 6-cijferige code uit uw authenticatie-app in om te verifiëren dat deze correct is ingesteld.",
-    "authCode": "Authenticatiecode", "authCodePlaceholder": "000000",
-    "back": "Terug", "verify": "Verifiëren",
+    "authCode": "Authenticatiecode",
+    "authCodePlaceholder": "000000",
+    "back": "Terug",
+    "verify": "Verifiëren",
     "enterCodeError": "Voer de code uit uw authenticatie-app in",
     "enterCodeError": "Voer de code uit uw authenticatie-app in",
-    "invalidCode": "Ongeldige code. Probeer het opnieuw.", "verificationFailed": "Verificatie mislukt",
+    "invalidCode": "Ongeldige code. Probeer het opnieuw.",
+    "verificationFailed": "Verificatie mislukt",
     "backupCodesTitle": "Back-upcodes opslaan",
     "backupCodesTitle": "Back-upcodes opslaan",
     "backupCodesDescription": "Bewaar deze codes op een veilige plek. U kunt ze gebruiken om in te loggen als u de toegang tot uw authenticatie-app verliest. Elke code kan slechts één keer worden gebruikt.",
     "backupCodesDescription": "Bewaar deze codes op een veilige plek. U kunt ze gebruiken om in te loggen als u de toegang tot uw authenticatie-app verliest. Elke code kan slechts één keer worden gebruikt.",
-    "copyCodes": "Codes kopiëren", "copied": "Gekopieerd",
+    "copyCodes": "Codes kopiëren",
+    "copied": "Gekopieerd",
     "backupCodesCopied": "Back-upcodes gekopieerd naar klembord",
     "backupCodesCopied": "Back-upcodes gekopieerd naar klembord",
     "savedBackupCodes": "Ik heb mijn back-upcodes opgeslagen",
     "savedBackupCodes": "Ik heb mijn back-upcodes opgeslagen",
     "twoFactorEnabledSuccess": "Tweefactorauthenticatie ingeschakeld",
     "twoFactorEnabledSuccess": "Tweefactorauthenticatie ingeschakeld",
@@ -117,7 +195,8 @@
     "deleteAccountDialogDescription": "Dit verwijdert uw account en alle bijbehorende gegevens permanent. Deze actie is onomkeerbaar.",
     "deleteAccountDialogDescription": "Dit verwijdert uw account en alle bijbehorende gegevens permanent. Deze actie is onomkeerbaar.",
     "deleteAccountConfirmPhrase": "delete me",
     "deleteAccountConfirmPhrase": "delete me",
     "deleteAccountConfirmPrompt": "Typ <bold>delete me</bold> om te bevestigen",
     "deleteAccountConfirmPrompt": "Typ <bold>delete me</bold> om te bevestigen",
-    "permanentlyDelete": "Permanent verwijderen", "deleting": "Verwijderen...",
+    "permanentlyDelete": "Permanent verwijderen",
+    "deleting": "Verwijderen...",
     "failedDeleteAccount": "Account verwijderen mislukt",
     "failedDeleteAccount": "Account verwijderen mislukt",
     "deleteContentDialogTitle": "Inhoud verwijderen",
     "deleteContentDialogTitle": "Inhoud verwijderen",
     "deleteContentDialogDescription": "Selecteer de inhoud die u permanent wilt verwijderen. Deze actie kan niet ongedaan worden gemaakt.",
     "deleteContentDialogDescription": "Selecteer de inhoud die u permanent wilt verwijderen. Deze actie kan niet ongedaan worden gemaakt.",
@@ -125,8 +204,10 @@
     "contentVehiclesDescription": "Alle voertuigen en hun servicegegevens, notities, herinneringen, brandstoflogboeken en terugkerende facturen",
     "contentVehiclesDescription": "Alle voertuigen en hun servicegegevens, notities, herinneringen, brandstoflogboeken en terugkerende facturen",
     "contentCustomers": "Klanten",
     "contentCustomers": "Klanten",
     "contentCustomersDescription": "Alle klantenrecords. Voertuigen gekoppeld aan verwijderde klanten worden ontkoppeld.",
     "contentCustomersDescription": "Alle klantenrecords. Voertuigen gekoppeld aan verwijderde klanten worden ontkoppeld.",
-    "contentQuotes": "Offertes", "contentQuotesDescription": "Alle offertes en hun regels",
-    "contentInventory": "Inventaris", "contentInventoryDescription": "Alle inventarisonderdelen en hun afbeeldingen",
+    "contentQuotes": "Offertes",
+    "contentQuotesDescription": "Alle offertes en hun regels",
+    "contentInventory": "Inventaris",
+    "contentInventoryDescription": "Alle inventarisonderdelen en hun afbeeldingen",
     "deleteContentConfirmPhrase": "delete my data",
     "deleteContentConfirmPhrase": "delete my data",
     "deleteContentConfirmPrompt": "Typ <bold>delete my data</bold> om te bevestigen",
     "deleteContentConfirmPrompt": "Typ <bold>delete my data</bold> om te bevestigen",
     "deleteSelected": "{count} geselecteerde verwijderen",
     "deleteSelected": "{count} geselecteerde verwijderen",
@@ -162,38 +243,54 @@
     "orgDescription": "Beheer de leden van uw organisatie en hun rollen.",
     "orgDescription": "Beheer de leden van uw organisatie en hun rollen.",
     "createOrg": "Organisatie aanmaken",
     "createOrg": "Organisatie aanmaken",
     "createOrgDescription": "Maak een organisatie aan om teamleden uit te nodigen. U wordt de eigenaar.",
     "createOrgDescription": "Maak een organisatie aan om teamleden uit te nodigen. U wordt de eigenaar.",
-    "orgName": "Organisatienaam", "orgNamePlaceholder": "Mijn werkplaats",
-    "create": "Aanmaken", "orgCreated": "Organisatie aangemaakt",
+    "orgName": "Organisatienaam",
+    "orgNamePlaceholder": "Mijn werkplaats",
+    "create": "Aanmaken",
+    "orgCreated": "Organisatie aangemaakt",
     "failedCreateOrg": "Organisatie aanmaken mislukt",
     "failedCreateOrg": "Organisatie aanmaken mislukt",
-    "memberCount": "{count} lid", "memberCountPlural": "{count} leden",
-    "admin": "Beheerder", "member": "Lid",
-    "inviteByEmail": "Uitnodigen via e-mail", "inviteEmailPlaceholder": "gebruiker@voorbeeld.nl",
-    "invite": "Uitnodigen", "invited": "Lid succesvol uitgenodigd.",
+    "memberCount": "{count} lid",
+    "memberCountPlural": "{count} leden",
+    "admin": "Beheerder",
+    "member": "Lid",
+    "inviteByEmail": "Uitnodigen via e-mail",
+    "inviteEmailPlaceholder": "gebruiker@voorbeeld.nl",
+    "invite": "Uitnodigen",
+    "invited": "Lid succesvol uitgenodigd.",
     "userNotFoundTitle": "Gebruiker niet gevonden",
     "userNotFoundTitle": "Gebruiker niet gevonden",
     "userNotFoundDescription": "Geen account gevonden voor \"{email}\". Een uitnodigingsmail sturen om zich te registreren en lid te worden van uw team?",
     "userNotFoundDescription": "Geen account gevonden voor \"{email}\". Een uitnodigingsmail sturen om zich te registreren en lid te worden van uw team?",
     "sendInvitation": "Uitnodiging sturen",
     "sendInvitation": "Uitnodiging sturen",
     "invitationSentTitle": "Uitnodiging verstuurd",
     "invitationSentTitle": "Uitnodiging verstuurd",
     "invitationSentDescription": "Een uitnodigingsmail is verstuurd naar {email}.",
     "invitationSentDescription": "Een uitnodigingsmail is verstuurd naar {email}.",
-    "failedInvite": "Lid uitnodigen mislukt", "failedSendInvitation": "Uitnodiging versturen mislukt",
+    "failedInvite": "Lid uitnodigen mislukt",
+    "failedSendInvitation": "Uitnodiging versturen mislukt",
     "pendingInvitations": "Openstaande uitnodigingen",
     "pendingInvitations": "Openstaande uitnodigingen",
-    "invitedAs": "Uitgenodigd als {role}", "expires": "Verloopt {date}",
-    "pending": "Openstaand", "copyInviteLink": "Uitnodigingslink kopiëren",
+    "invitedAs": "Uitgenodigd als {role}",
+    "expires": "Verloopt {date}",
+    "pending": "Openstaand",
+    "copyInviteLink": "Uitnodigingslink kopiëren",
     "inviteLinkCopied": "Uitnodigingslink gekopieerd naar klembord",
     "inviteLinkCopied": "Uitnodigingslink gekopieerd naar klembord",
     "cancelInvitation": "Uitnodiging annuleren",
     "cancelInvitation": "Uitnodiging annuleren",
     "cancelInvitationDescription": "Uitnodiging naar {email} annuleren? Ze kunnen deze link niet meer gebruiken.",
     "cancelInvitationDescription": "Uitnodiging naar {email} annuleren? Ze kunnen deze link niet meer gebruiken.",
-    "invitationCancelled": "Uitnodiging geannuleerd", "failedCancelInvitation": "Uitnodiging annuleren mislukt",
-    "customRoles": "Aangepaste rollen", "newRole": "Nieuwe rol",
-    "roleName": "Rolnaam", "roleNamePlaceholder": "Bijv. Monteur",
+    "invitationCancelled": "Uitnodiging geannuleerd",
+    "failedCancelInvitation": "Uitnodiging annuleren mislukt",
+    "customRoles": "Aangepaste rollen",
+    "newRole": "Nieuwe rol",
+    "roleName": "Rolnaam",
+    "roleNamePlaceholder": "Bijv. Monteur",
     "fullAdminAccess": "Volledige beheerderstoegang (omzeilt alle machtigingscontroles)",
     "fullAdminAccess": "Volledige beheerderstoegang (omzeilt alle machtigingscontroles)",
     "permissions": "Machtigingen",
     "permissions": "Machtigingen",
-    "createRole": "Rol aanmaken", "updateRole": "Rol bijwerken",
-    "roleCreated": "Rol aangemaakt", "roleUpdated": "Rol bijgewerkt",
+    "createRole": "Rol aanmaken",
+    "updateRole": "Rol bijwerken",
+    "roleCreated": "Rol aangemaakt",
+    "roleUpdated": "Rol bijgewerkt",
     "failedSaveRole": "Rol opslaan mislukt",
     "failedSaveRole": "Rol opslaan mislukt",
     "deleteRole": "Rol verwijderen",
     "deleteRole": "Rol verwijderen",
     "deleteRoleDescription": "Rol \"{name}\" verwijderen? Leden met deze rol verliezen hun aangepaste machtigingen.",
     "deleteRoleDescription": "Rol \"{name}\" verwijderen? Leden met deze rol verliezen hun aangepaste machtigingen.",
-    "roleDeleted": "Rol verwijderd", "failedDeleteRole": "Rol verwijderen mislukt",
+    "roleDeleted": "Rol verwijderd",
+    "failedDeleteRole": "Rol verwijderen mislukt",
     "fullAccess": "Volledige toegang",
     "fullAccess": "Volledige toegang",
-    "permissionCount": "{count} machtiging", "permissionCountPlural": "{count} machtigingen",
+    "permissionCount": "{count} machtiging",
+    "permissionCountPlural": "{count} machtigingen",
     "noCustomRoles": "Nog geen aangepaste rollen. Maak er een aan om gedetailleerde machtigingen voor teamleden te definiëren.",
     "noCustomRoles": "Nog geen aangepaste rollen. Maak er een aan om gedetailleerde machtigingen voor teamleden te definiëren.",
     "builtInRoles": "Ingebouwde rollen",
     "builtInRoles": "Ingebouwde rollen",
     "ownerLabel": "Eigenaar",
     "ownerLabel": "Eigenaar",
@@ -202,60 +299,165 @@
     "adminDescription": "Kan leden uitnodigen/verwijderen en gedeelde gegevens beheren.",
     "adminDescription": "Kan leden uitnodigen/verwijderen en gedeelde gegevens beheren.",
     "memberLabel": "Lid",
     "memberLabel": "Lid",
     "memberDescription": "Machtigingen bepaald door toegewezen aangepaste rol. Geen aangepaste rol betekent alleen-lezen toegang.",
     "memberDescription": "Machtigingen bepaald door toegewezen aangepaste rol. Geen aangepaste rol betekent alleen-lezen toegang.",
-    "roleAssigned": "Rol bijgewerkt", "failedAssignRole": "Rol bijwerken mislukt",
+    "roleAssigned": "Rol bijgewerkt",
+    "failedAssignRole": "Rol bijwerken mislukt",
     "removeMemberTitle": "Lid verwijderen",
     "removeMemberTitle": "Lid verwijderen",
     "removeMemberDescription": "{name} uit het team verwijderen? Toegang tot gedeelde gegevens gaat verloren.",
     "removeMemberDescription": "{name} uit het team verwijderen? Toegang tot gedeelde gegevens gaat verloren.",
-    "removeButton": "Verwijderen", "memberRemoved": "Lid verwijderd",
+    "removeButton": "Verwijderen",
+    "memberRemoved": "Lid verwijderd",
     "failedRemoveMember": "Lid verwijderen mislukt",
     "failedRemoveMember": "Lid verwijderen mislukt",
-    "memberRoleUpdated": "Rol van lid bijgewerkt", "failedUpdateRole": "Rol bijwerken mislukt"
+    "memberRoleUpdated": "Rol van lid bijgewerkt",
+    "failedUpdateRole": "Rol bijwerken mislukt"
   },
   },
   "invoice": {
   "invoice": {
     "title": "Factuurindeling",
     "title": "Factuurindeling",
     "description": "Configureer wat er op gegenereerde PDF-facturen verschijnt.",
     "description": "Configureer wat er op gegenereerde PDF-facturen verschijnt.",
+    "tabs": {
+      "general": "Algemeen",
+      "layout": "Layout",
+      "customFields": "Aangepaste velden"
+    },
     "invoiceNumberFormat": "Factuurnummerformaat",
     "invoiceNumberFormat": "Factuurnummerformaat",
     "invoiceNumberFormatHint": "Gebruik <code>{year}</code> voor het huidige jaar. Voorbeeld: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Gebruik <code>{year}</code> voor het huidige jaar. Voorbeeld: <bold>{preview}</bold>",
-    "nextInvoiceNumber": "Volgend factuurnummer", "nextInvoiceNumberPlaceholder": "Bijv. 94",
+    "nextInvoiceNumber": "Volgend factuurnummer",
+    "nextInvoiceNumberPlaceholder": "Bijv. 94",
     "nextInvoiceNumberHint": "De volgende factuur gebruikt dit nummer (bijv. {example})",
     "nextInvoiceNumberHint": "De volgende factuur gebruikt dit nummer (bijv. {example})",
-    "dueDays": "Vervaldagen", "dueDaysHint": "Aantal dagen tot de factuur vervalt",
+    "dueDays": "Vervaldagen",
+    "dueDaysHint": "Aantal dagen tot de factuur vervalt",
     "visibilityTitle": "Zichtbaarheid op factuur",
     "visibilityTitle": "Zichtbaarheid op factuur",
-    "companyLogo": "Bedrijfslogo", "companyLogoHint": "Toon bedrijfslogo in factuuraanhef",
-    "companyName": "Bedrijfsnaam", "companyNameHint": "Toon bedrijfsnaam in factuuraanhef",
-    "bankAccount": "Bankrekening", "bankAccountHint": "Toon bankrekening / IBAN op factuur",
-    "organizationNumber": "Organisatienummer", "organizationNumberHint": "Toon organisatienummer in aanhef",
+    "companyLogo": "Bedrijfslogo",
+    "companyLogoHint": "Toon bedrijfslogo in factuuraanhef",
+    "companyName": "Bedrijfsnaam",
+    "companyNameHint": "Toon bedrijfsnaam in factuuraanhef",
+    "bankAccount": "Bankrekening",
+    "bankAccountHint": "Toon bankrekening / IBAN op factuur",
+    "organizationNumber": "Organisatienummer",
+    "organizationNumberHint": "Toon organisatienummer in aanhef",
     "customFooter": "Aangepaste factuurvoettekst",
     "customFooter": "Aangepaste factuurvoettekst",
     "footerPlaceholder": "Bedankt voor uw vertrouwen!",
     "footerPlaceholder": "Bedankt voor uw vertrouwen!",
     "footerHint": "Deze tekst verschijnt onderaan elke factuur",
     "footerHint": "Deze tekst verschijnt onderaan elke factuur",
-    "saveInvoice": "Factuurinstellingen opslaan", "saved": "Factuurinstellingen opgeslagen"
+    "saveInvoice": "Factuurinstellingen opslaan",
+    "saved": "Factuurinstellingen opgeslagen",
+    "layoutDescription": "Pas de volgorde en zichtbaarheid van secties in uw facturen en offertes aan.",
+    "layoutSaved": "Layout succesvol opgeslagen",
+    "preview": "Voorbeeld",
+    "layoutDocInvoice": "Factuur",
+    "layoutDocQuote": "Offerte",
+    "saveLayout": "Layout opslaan",
+    "resetLayout": "Herstellen"
+  },
+  "layoutEditor": {
+    "helpText": "Sleep om te herordenen. Gebruik de breedte-schakelaar om secties naast elkaar te plaatsen. Opeenvolgende halve breedte secties (L/R) worden in twee kolommen weergegeven.",
+    "addField": "Veld toevoegen",
+    "custom": "aangepast",
+    "full": "Volledig",
+    "L": "L",
+    "R": "R",
+    "left": "links",
+    "right": "rechts",
+    "halfWidthHint": "Halve breedte ({column}) — klik voor volledige breedte",
+    "fullWidthHint": "Volledige breedte — klik voor halve breedte (links)",
+    "sections": {
+      "header": "Koptekst",
+      "customer": "Klant",
+      "vehicle": "Voertuig",
+      "service": "Service",
+      "parts_table": "Onderdelen",
+      "labor_table": "Arbeid",
+      "totals": "Totalen",
+      "notes": "Notities",
+      "diagnostic_notes": "Diagnostische notities",
+      "bank_account": "Bankrekening",
+      "footer": "Voettekst",
+      "general": "Algemeen"
+    },
+    "fields": {
+      "customer_name": "Klantnaam",
+      "customer_company": "Bedrijf",
+      "customer_address": "Adres",
+      "customer_email": "E-mail",
+      "customer_phone": "Telefoon",
+      "vehicle_name": "Voertuig",
+      "vin": "VIN",
+      "license_plate": "Kenteken",
+      "mileage": "Kilometerstand",
+      "service_title": "Servicetitel",
+      "service_type": "Servicetype",
+      "tech_name": "Monteur",
+      "logo": "Logo",
+      "company_name": "Bedrijfsnaam",
+      "bank_account": "Bankrekening",
+      "org_number": "Organisatienummer"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Sjablonen",
     "title": "Sjablonen",
     "invoiceDescription": "Pas het uiterlijk van uw PDF-facturen en offertes aan.",
     "invoiceDescription": "Pas het uiterlijk van uw PDF-facturen en offertes aan.",
     "inspectionsDescription": "Beheer inspectiechecklists en meerpuntssjablonen.",
     "inspectionsDescription": "Beheer inspectiechecklists en meerpuntssjablonen.",
     "smsDescription": "Pas de SMS-berichten aan die naar klanten worden gestuurd.",
     "smsDescription": "Pas de SMS-berichten aan die naar klanten worden gestuurd.",
-    "tabs": { "invoice": "Factuur", "quotation": "Offerte", "inspections": "Inspecties", "sms": "SMS" },
-    "presets": "Sjabloonpresets", "primaryColor": "Primaire kleur",
-    "fontAndLayout": "Lettertype en indeling", "fontFamily": "Letterfamilie",
-    "helveticaDefault": "Helvetica (Standaard)", "timesRoman": "Times Roman", "courier": "Courier",
-    "headerStyle": "Aanhefstijl", "standard": "Standaard", "compact": "Compact", "modern": "Modern",
-    "preview": "{name} voorbeeld", "billTo": "Factuuradres", "preparedFor": "Opgesteld voor",
-    "vehicle": "Voertuig", "part": "Onderdeel", "qty": "Aantal", "price": "Prijs", "total": "Totaal",
-    "labor": "Arbeid", "hours": "Uren", "rate": "Tarief", "subtotal": "Subtotaal", "tax": "BTW ({rate}%)",
-    "saveInvoiceTemplate": "Factuursjabloon opslaan", "saveQuotationTemplate": "Offertesjabloon opslaan",
+    "tabs": {
+      "invoice": "Factuur",
+      "quotation": "Offerte",
+      "inspections": "Inspecties",
+      "sms": "SMS"
+    },
+    "presets": "Sjabloonpresets",
+    "primaryColor": "Primaire kleur",
+    "fontAndLayout": "Lettertype en indeling",
+    "fontFamily": "Letterfamilie",
+    "helveticaDefault": "Helvetica (Standaard)",
+    "timesRoman": "Times Roman",
+    "courier": "Courier",
+    "headerStyle": "Aanhefstijl",
+    "logoSize": "Logogrootte",
+    "standard": "Standaard",
+    "compact": "Compact",
+    "modern": "Modern",
+    "preview": "{name} voorbeeld",
+    "billTo": "Factuuradres",
+    "preparedFor": "Opgesteld voor",
+    "vehicle": "Voertuig",
+    "part": "Onderdeel",
+    "qty": "Aantal",
+    "price": "Prijs",
+    "total": "Totaal",
+    "labor": "Arbeid",
+    "hours": "Uren",
+    "rate": "Tarief",
+    "subtotal": "Subtotaal",
+    "tax": "BTW ({rate}%)",
+    "previewService": "Service",
+    "previewNotes": "Opmerkingen",
+    "previewDiagnosticNotes": "Diagnostische notities",
+    "previewDue": "Vervaldatum: {date}",
+    "previewAdditionalInfo": "Aanvullende informatie",
+    "previewThankYou": "Bedankt voor uw vertrouwen",
+    "saveInvoiceTemplate": "Factuursjabloon opslaan",
+    "saveQuotationTemplate": "Offertesjabloon opslaan",
     "invoiceTemplateSaved": "Factuursjablooninstellingen opgeslagen",
     "invoiceTemplateSaved": "Factuursjablooninstellingen opgeslagen",
     "quotationTemplateSaved": "Offertesjablooninstellingen opgeslagen",
     "quotationTemplateSaved": "Offertesjablooninstellingen opgeslagen",
     "failedSave": "Instellingen opslaan mislukt",
     "failedSave": "Instellingen opslaan mislukt",
     "smsTemplates": "SMS-berichtsjablonen",
     "smsTemplates": "SMS-berichtsjablonen",
     "smsTemplatesDescription": "Pas de tekstberichten aan die naar klanten worden gestuurd. Gebruik variabelen zoals <code>{companyName}</code> om dynamische waarden in te voegen.",
     "smsTemplatesDescription": "Pas de tekstberichten aan die naar klanten worden gestuurd. Gebruik variabelen zoals <code>{companyName}</code> om dynamische waarden in te voegen.",
     "reset": "Resetten",
     "reset": "Resetten",
-    "invoiceReady": "Factuur gereed", "invoiceReadyDescription": "Verzonden bij het delen van een factuur met een klant.",
-    "quoteReady": "Offerte gereed", "quoteReadyDescription": "Verzonden bij het delen van een offerte met een klant.",
-    "inspectionReady": "Inspectie gereed", "inspectionReadyDescription": "Verzonden bij het delen van een inspectierapport.",
-    "statusInProgress": "Status: In uitvoering", "statusInProgressDescription": "Verzonden wanneer de werkorderstatus wijzigt naar In uitvoering.",
-    "statusWaitingParts": "Status: Wachten op onderdelen", "statusWaitingPartsDescription": "Verzonden wanneer de status wijzigt naar Wachten op onderdelen.",
-    "statusReady": "Status: Klaar voor ophalen", "statusReadyDescription": "Verzonden wanneer de status wijzigt naar Gereed.",
-    "statusCompleted": "Status: Voltooid", "statusCompletedDescription": "Verzonden wanneer de status wijzigt naar Voltooid.",
-    "paymentReceived": "Betaling ontvangen", "paymentReceivedDescription": "Verzonden wanneer een betaling wordt geregistreerd.",
-    "saveSmsTemplates": "SMS-sjablonen opslaan", "smsTemplateSaved": "SMS-sjablooninstellingen opgeslagen",
+    "invoiceReady": "Factuur gereed",
+    "invoiceReadyDescription": "Verzonden bij het delen van een factuur met een klant.",
+    "quoteReady": "Offerte gereed",
+    "quoteReadyDescription": "Verzonden bij het delen van een offerte met een klant.",
+    "inspectionReady": "Inspectie gereed",
+    "inspectionReadyDescription": "Verzonden bij het delen van een inspectierapport.",
+    "statusInProgress": "Status: In uitvoering",
+    "statusInProgressDescription": "Verzonden wanneer de werkorderstatus wijzigt naar In uitvoering.",
+    "statusWaitingParts": "Status: Wachten op onderdelen",
+    "statusWaitingPartsDescription": "Verzonden wanneer de status wijzigt naar Wachten op onderdelen.",
+    "statusReady": "Status: Klaar voor ophalen",
+    "statusReadyDescription": "Verzonden wanneer de status wijzigt naar Gereed.",
+    "statusCompleted": "Status: Voltooid",
+    "statusCompletedDescription": "Verzonden wanneer de status wijzigt naar Voltooid.",
+    "paymentReceived": "Betaling ontvangen",
+    "paymentReceivedDescription": "Verzonden wanneer een betaling wordt geregistreerd.",
+    "saveSmsTemplates": "SMS-sjablonen opslaan",
+    "smsTemplateSaved": "SMS-sjablooninstellingen opgeslagen",
     "smsDefaults": {
     "smsDefaults": {
       "invoiceReady": "Uw factuur is gereed. Bekijk deze hier: {share_link}",
       "invoiceReady": "Uw factuur is gereed. Bekijk deze hier: {share_link}",
       "quoteReady": "Uw offerte is klaar voor beoordeling. Bekijk deze hier: {share_link}",
       "quoteReady": "Uw offerte is klaar voor beoordeling. Bekijk deze hier: {share_link}",
@@ -267,55 +469,99 @@
       "paymentReceived": "Betaling van {amount} ontvangen voor factuur {invoice_number}. Bedankt!"
       "paymentReceived": "Betaling van {amount} ontvangen voor factuur {invoice_number}. Bedankt!"
     },
     },
     "colorPresets": {
     "colorPresets": {
-      "amber": "Amber", "blue": "Blauw", "emerald": "Smaragd", "red": "Rood",
-      "purple": "Paars", "slate": "Leisteen", "rose": "Roze", "indigo": "Indigo"
+      "amber": "Amber",
+      "blue": "Blauw",
+      "emerald": "Smaragd",
+      "red": "Rood",
+      "purple": "Paars",
+      "slate": "Leisteen",
+      "rose": "Roze",
+      "indigo": "Indigo"
     }
     }
   },
   },
   "payment": {
   "payment": {
     "detailsTitle": "Betalingsgegevens",
     "detailsTitle": "Betalingsgegevens",
     "detailsDescription": "Bankrekening en betalingsvoorwaarden op facturen.",
     "detailsDescription": "Bankrekening en betalingsvoorwaarden op facturen.",
-    "bankAccount": "Bankrekening (IBAN / Til Konto)", "bankAccountPlaceholder": "NO93 8601 1117 947",
+    "bankAccount": "Bankrekening (IBAN / Til Konto)",
+    "bankAccountPlaceholder": "NO93 8601 1117 947",
     "bankAccountHint": "Uw bankrekeningnummer of IBAN voor het ontvangen van betalingen",
     "bankAccountHint": "Uw bankrekeningnummer of IBAN voor het ontvangen van betalingen",
-    "paymentTerms": "Betalingsvoorwaarden", "paymentTermsPlaceholder": "30 dagen netto",
+    "paymentTerms": "Betalingsvoorwaarden",
+    "paymentTermsPlaceholder": "30 dagen netto",
     "paymentTermsHint": "Betalingsvoorwaarden op facturen (bijv. \"30 dagen netto\", \"Bij ontvangst\")",
     "paymentTermsHint": "Betalingsvoorwaarden op facturen (bijv. \"30 dagen netto\", \"Bij ontvangst\")",
     "onlinePayments": "Online betalingen",
     "onlinePayments": "Online betalingen",
     "onlinePaymentsDescription": "Laat klanten facturen online betalen via gedeelde factuurlinks.",
     "onlinePaymentsDescription": "Laat klanten facturen online betalen via gedeelde factuurlinks.",
-    "stripe": "Stripe", "stripeDescription": "Creditcard-/debitcardbetalingen accepteren",
-    "secretKey": "Geheime sleutel", "publishableKey": "Publieke sleutel",
-    "webhookSecret": "Webhook-geheim", "webhookUrl": "Webhook-URL",
+    "stripe": "Stripe",
+    "stripeDescription": "Creditcard-/debitcardbetalingen accepteren",
+    "secretKey": "Geheime sleutel",
+    "publishableKey": "Publieke sleutel",
+    "webhookSecret": "Webhook-geheim",
+    "webhookUrl": "Webhook-URL",
     "stripeWebhookHint": "Voeg deze URL toe in uw Stripe Dashboard onder Webhooks. Abonneer op <code>checkout.session.completed</code>.",
     "stripeWebhookHint": "Voeg deze URL toe in uw Stripe Dashboard onder Webhooks. Abonneer op <code>checkout.session.completed</code>.",
-    "vipps": "Vipps", "vippsDescription": "Vipps mobiele betalingen accepteren (Noorwegen)",
-    "clientId": "Client-ID", "clientSecret": "Client-geheim",
-    "subscriptionKey": "Abonnementssleutel", "merchantSerialNumber": "Handelaarserienummer",
-    "testMode": "Testmodus", "callbackUrl": "Callback-URL",
+    "vipps": "Vipps",
+    "vippsDescription": "Vipps mobiele betalingen accepteren (Noorwegen)",
+    "clientId": "Client-ID",
+    "clientSecret": "Client-geheim",
+    "subscriptionKey": "Abonnementssleutel",
+    "merchantSerialNumber": "Handelaarserienummer",
+    "testMode": "Testmodus",
+    "callbackUrl": "Callback-URL",
     "termsOfSale": "Verkoopvoorwaarden (Salgsvilkar)",
     "termsOfSale": "Verkoopvoorwaarden (Salgsvilkar)",
     "termsOfSaleDescription": "Vereist door Vipps. Klanten kunnen deze bekijken vanaf de factuurpagina.",
     "termsOfSaleDescription": "Vereist door Vipps. Klanten kunnen deze bekijken vanaf de factuurpagina.",
     "externalTermsUrl": "Externe voorwaarden-URL (optioneel)",
     "externalTermsUrl": "Externe voorwaarden-URL (optioneel)",
     "externalTermsUrlHint": "Link naar een externe pagina met uw voorwaarden.",
     "externalTermsUrlHint": "Link naar een externe pagina met uw voorwaarden.",
     "publicTermsUrl": "Publieke voorwaarden-URL",
     "publicTermsUrl": "Publieke voorwaarden-URL",
     "publicTermsUrlHint": "Deel deze URL met Vipps of link ernaar vanaf uw website.",
     "publicTermsUrlHint": "Deel deze URL met Vipps of link ernaar vanaf uw website.",
-    "paypal": "PayPal", "paypalDescription": "PayPal-betalingen accepteren",
+    "paypal": "PayPal",
+    "paypalDescription": "PayPal-betalingen accepteren",
     "sandboxMode": "Sandboxmodus",
     "sandboxMode": "Sandboxmodus",
     "paypalWebhookHint": "Voeg deze URL toe in uw PayPal Developer Dashboard onder Webhooks. Abonneer op <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "paypalWebhookHint": "Voeg deze URL toe in uw PayPal Developer Dashboard onder Webhooks. Abonneer op <code>PAYMENT.CAPTURE.COMPLETED</code>.",
-    "savePayment": "Betalingsinstellingen opslaan", "saved": "Betalingsinstellingen opgeslagen"
+    "savePayment": "Betalingsinstellingen opslaan",
+    "saved": "Betalingsinstellingen opgeslagen"
   },
   },
   "currency": {
   "currency": {
     "title": "Valuta en belasting",
     "title": "Valuta en belasting",
     "description": "Standaardvaluta en belastingtarief voor facturen en servicerecords.",
     "description": "Standaardvaluta en belastingtarief voor facturen en servicerecords.",
-    "currencyLabel": "Valuta", "previewLabel": "Voorbeeld: {value}",
-    "enableTax": "Belasting inschakelen", "enableTaxHint": "Toon belastingberekening op facturen en servicerecords",
+    "currencyLabel": "Valuta",
+    "previewLabel": "Voorbeeld: {value}",
+    "enableTax": "Belasting inschakelen",
+    "enableTaxHint": "Toon belastingberekening op facturen en servicerecords",
     "defaultTaxRate": "Standaard belastingtarief (%)",
     "defaultTaxRate": "Standaard belastingtarief (%)",
     "defaultTaxRateHint": "Automatisch ingevuld bij het aanmaken van nieuwe servicerecords",
     "defaultTaxRateHint": "Automatisch ingevuld bij het aanmaken van nieuwe servicerecords",
-    "saveSettings": "Instellingen opslaan", "saved": "Instellingen opgeslagen",
+    "saveSettings": "Instellingen opslaan",
+    "saved": "Instellingen opgeslagen",
     "currencies": {
     "currencies": {
-      "USD": "Amerikaanse dollar", "EUR": "Euro", "GBP": "Brits pond", "NOK": "Noorse kroon",
-      "SEK": "Zweedse kroon", "DKK": "Deense kroon", "CHF": "Zwitserse frank", "CAD": "Canadese dollar",
-      "AUD": "Australische dollar", "NZD": "Nieuw-Zeelandse dollar", "JPY": "Japanse yen", "CNY": "Chinese yuan",
-      "INR": "Indiase roepie", "BRL": "Braziliaanse real", "MXN": "Mexicaanse peso", "PLN": "Poolse zloty",
-      "CZK": "Tsjechische kroon", "HUF": "Hongaarse forint", "TRY": "Turkse lira", "ZAR": "Zuid-Afrikaanse rand",
-      "KRW": "Zuid-Koreaanse won", "SGD": "Singaporese dollar", "HKD": "Hongkongse dollar", "THB": "Thaise baht",
-      "ISK": "IJslandse kroon", "RON": "Roemeense leu", "ILS": "Israëlische sjekel", "PHP": "Filipijnse peso",
-      "IDR": "Indonesische roepia", "MYR": "Maleisische ringgit", "AED": "VAE-dirham", "SAR": "Saoedische riyal"
+      "USD": "Amerikaanse dollar",
+      "EUR": "Euro",
+      "GBP": "Brits pond",
+      "NOK": "Noorse kroon",
+      "SEK": "Zweedse kroon",
+      "DKK": "Deense kroon",
+      "CHF": "Zwitserse frank",
+      "CAD": "Canadese dollar",
+      "AUD": "Australische dollar",
+      "NZD": "Nieuw-Zeelandse dollar",
+      "JPY": "Japanse yen",
+      "CNY": "Chinese yuan",
+      "INR": "Indiase roepie",
+      "BRL": "Braziliaanse real",
+      "MXN": "Mexicaanse peso",
+      "PLN": "Poolse zloty",
+      "CZK": "Tsjechische kroon",
+      "HUF": "Hongaarse forint",
+      "TRY": "Turkse lira",
+      "ZAR": "Zuid-Afrikaanse rand",
+      "KRW": "Zuid-Koreaanse won",
+      "SGD": "Singaporese dollar",
+      "HKD": "Hongkongse dollar",
+      "THB": "Thaise baht",
+      "ISK": "IJslandse kroon",
+      "RON": "Roemeense leu",
+      "ILS": "Israëlische sjekel",
+      "PHP": "Filipijnse peso",
+      "IDR": "Indonesische roepia",
+      "MYR": "Maleisische ringgit",
+      "AED": "VAE-dirham",
+      "SAR": "Saoedische riyal"
     }
     }
   },
   },
   "customFields": {
   "customFields": {
@@ -362,65 +608,98 @@
   "workshop": {
   "workshop": {
     "title": "Werkplaatsstandaarden",
     "title": "Werkplaatsstandaarden",
     "description": "Standaardwaarden bij het aanmaken van nieuwe servicerecords.",
     "description": "Standaardwaarden bij het aanmaken van nieuwe servicerecords.",
-    "defaultTechnician": "Standaard monteursnaam", "technicianPlaceholder": "Zoek monteur...",
+    "defaultTechnician": "Standaard monteursnaam",
+    "technicianPlaceholder": "Zoek monteur...",
     "failedCreateTech": "Kan monteur niet aanmaken",
     "failedCreateTech": "Kan monteur niet aanmaken",
     "creating": "Aanmaken...",
     "creating": "Aanmaken...",
     "createTechnician": "\"{name}\" aanmaken",
     "createTechnician": "\"{name}\" aanmaken",
     "newTechPlaceholder": "Naam monteur",
     "newTechPlaceholder": "Naam monteur",
     "addTech": "Toevoegen",
     "addTech": "Toevoegen",
     "addNewTech": "+ Nieuw toevoegen",
     "addNewTech": "+ Nieuw toevoegen",
-    "defaultLaborRate": "Standaard uurtarief", "laborRatePlaceholder": "75,00",
-    "workingHours": "Werkuren", "workingHoursPlaceholder": "Ma-Vr 8:00 - 17:00",
-    "unitsTitle": "Eenheden", "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",
-    "unitSystem": "Eenhedensysteem", "metric": "Metrisch (km, liters)", "imperial": "Imperiaal (mijlen, gallons)",
-    "distanceLabel": "Afstand", "volumeLabel": "Volume",
-    "kilometers": "kilometers (km)", "liters": "liters (L)", "miles": "mijlen (mi)", "gallons": "gallons (gal)",
-    "saveWorkshop": "Werkplaatsinstellingen opslaan", "saved": "Werkplaatsinstellingen opgeslagen"
+    "defaultLaborRate": "Standaard uurtarief",
+    "laborRatePlaceholder": "75,00",
+    "workingHours": "Werkuren",
+    "workingHoursPlaceholder": "Ma-Vr 8:00 - 17:00",
+    "unitsTitle": "Eenheden",
+    "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",
+    "unitSystem": "Eenhedensysteem",
+    "metric": "Metrisch (km, liters)",
+    "imperial": "Imperiaal (mijlen, gallons)",
+    "distanceLabel": "Afstand",
+    "volumeLabel": "Volume",
+    "kilometers": "kilometers (km)",
+    "liters": "liters (L)",
+    "miles": "mijlen (mi)",
+    "gallons": "gallons (gal)",
+    "saveWorkshop": "Werkplaatsinstellingen opslaan",
+    "saved": "Werkplaatsinstellingen opgeslagen"
   },
   },
   "maintenance": {
   "maintenance": {
     "title": "Voorspeld onderhoud",
     "title": "Voorspeld onderhoud",
     "description": "Voorspel de huidige kilometerstand van elk voertuig op basis van de servicehistorie en toon voertuigen die hun volgende service nodig hebben of naderen.",
     "description": "Voorspel de huidige kilometerstand van elk voertuig op basis van de servicehistorie en toon voertuigen die hun volgende service nodig hebben of naderen.",
     "enablePredicted": "Voorspeld onderhoud inschakelen",
     "enablePredicted": "Voorspeld onderhoud inschakelen",
     "enablePredictedHint": "Toon voorspellingen op voertuigdetails en dashboard",
     "enablePredictedHint": "Toon voorspellingen op voertuigdetails en dashboard",
-    "serviceInterval": "Service-interval ({unit})", "serviceIntervalHint": "Afstand tussen aanbevolen services",
+    "serviceInterval": "Service-interval ({unit})",
+    "serviceIntervalHint": "Afstand tussen aanbevolen services",
     "approachingThreshold": "Naderingsdrempel ({unit})",
     "approachingThreshold": "Naderingsdrempel ({unit})",
     "approachingThresholdHint": "Toon waarschuwing wanneer binnen deze afstand van de volgende service",
     "approachingThresholdHint": "Toon waarschuwing wanneer binnen deze afstand van de volgende service",
-    "saveMaintenance": "Onderhoudsinstellingen opslaan", "saved": "Onderhoudsinstellingen opgeslagen"
+    "saveMaintenance": "Onderhoudsinstellingen opslaan",
+    "saved": "Onderhoudsinstellingen opgeslagen"
   },
   },
   "appearance": {
   "appearance": {
-    "title": "Weergave", "description": "Pas aan hoe Torqvoice eruitziet.",
-    "darkMode": "Donkere modus", "darkModeHint": "Schakelen tussen licht en donker thema",
+    "title": "Weergave",
+    "description": "Pas aan hoe Torqvoice eruitziet.",
+    "darkMode": "Donkere modus",
+    "darkModeHint": "Schakelen tussen licht en donker thema",
     "dateTimeTitle": "Datum en tijd",
     "dateTimeTitle": "Datum en tijd",
     "dateTimeDescription": "Configureer hoe datums en tijden worden weergegeven in de applicatie.",
     "dateTimeDescription": "Configureer hoe datums en tijden worden weergegeven in de applicatie.",
-    "dateFormat": "Datumformaat", "timeFormat": "Tijdformaat",
-    "time12h": "12-uurs (2:30 PM)", "time24h": "24-uurs (14:30)",
-    "timezone": "Tijdzone", "timezoneAuto": "Automatisch detecteren (browser)",
+    "dateFormat": "Datumformaat",
+    "timeFormat": "Tijdformaat",
+    "time12h": "12-uurs (2:30 PM)",
+    "time24h": "24-uurs (14:30)",
+    "timezone": "Tijdzone",
+    "timezoneAuto": "Automatisch detecteren (browser)",
     "timezoneHint": "Bij automatische detectie ziet elke gebruiker datums in de tijdzone van de browser.",
     "timezoneHint": "Bij automatische detectie ziet elke gebruiker datums in de tijdzone van de browser.",
-    "previewLabel": "Voorbeeld", "dateLabel": "Datum", "dateTimeLabel": "Datum en tijd",
-    "saveSettings": "Instellingen opslaan", "saved": "Instellingen opgeslagen"
+    "previewLabel": "Voorbeeld",
+    "dateLabel": "Datum",
+    "dateTimeLabel": "Datum en tijd",
+    "saveSettings": "Instellingen opslaan",
+    "saved": "Instellingen opgeslagen"
   },
   },
   "data": {
   "data": {
     "title": "Gegevensbeheer",
     "title": "Gegevensbeheer",
     "description": "Exporteer uw gegevens als back-up of importeer een eerder geëxporteerd bestand.",
     "description": "Exporteer uw gegevens als back-up of importeer een eerder geëxporteerd bestand.",
     "exportTitle": "Gegevens exporteren",
     "exportTitle": "Gegevens exporteren",
     "exportDescription": "Download uw gegevens als zipbestand. Selecteer welke categorieën u wilt opnemen.",
     "exportDescription": "Download uw gegevens als zipbestand. Selecteer welke categorieën u wilt opnemen.",
-    "includeInBackup": "Opnemen in back-up:", "selectAll": "Alles selecteren", "deselectAll": "Alles deselecteren",
-    "optSettings": "Instellingen", "optSettingsDesc": "Werkplaatsinfo, factuurconfiguratie, betalingsproviders",
-    "optCustomers": "Klanten", "optCustomersDesc": "Klantenrecords en contactgegevens",
-    "optVehicles": "Voertuigen en servicerecords", "optVehiclesDesc": "Voertuigen, notities, brandstoflogboeken, herinneringen, servicerecords",
-    "optQuotes": "Offertes", "optQuotesDesc": "Offertes met onderdelen en arbeid",
-    "optInventory": "Inventaris", "optInventoryDesc": "Inventarisonderdelen en voorraadniveaus",
-    "optCustomFields": "Aangepaste velden", "optCustomFieldsDesc": "Aangepaste velddefinities en waarden",
-    "optFiles": "Geüploade bestanden", "optFilesDesc": "Afbeeldingen, servicebijlagen, logo",
+    "includeInBackup": "Opnemen in back-up:",
+    "selectAll": "Alles selecteren",
+    "deselectAll": "Alles deselecteren",
+    "optSettings": "Instellingen",
+    "optSettingsDesc": "Werkplaatsinfo, factuurconfiguratie, betalingsproviders",
+    "optCustomers": "Klanten",
+    "optCustomersDesc": "Klantenrecords en contactgegevens",
+    "optVehicles": "Voertuigen en servicerecords",
+    "optVehiclesDesc": "Voertuigen, notities, brandstoflogboeken, herinneringen, servicerecords",
+    "optQuotes": "Offertes",
+    "optQuotesDesc": "Offertes met onderdelen en arbeid",
+    "optInventory": "Inventaris",
+    "optInventoryDesc": "Inventarisonderdelen en voorraadniveaus",
+    "optCustomFields": "Aangepaste velden",
+    "optCustomFieldsDesc": "Aangepaste velddefinities en waarden",
+    "optFiles": "Geüploade bestanden",
+    "optFilesDesc": "Afbeeldingen, servicebijlagen, logo",
     "filesWarning": "Bevat geüploade bestanden — back-up kan groter zijn",
     "filesWarning": "Bevat geüploade bestanden — back-up kan groter zijn",
     "downloadBackup": "Back-up downloaden",
     "downloadBackup": "Back-up downloaden",
-    "exportComplete": "Export voltooid", "exportCompleteMessage": "Uw back-upbestand is gedownload.",
-    "exportFailed": "Export mislukt", "exportFailedMessage": "Kon uw gegevens niet exporteren. Probeer het opnieuw.",
+    "exportComplete": "Export voltooid",
+    "exportCompleteMessage": "Uw back-upbestand is gedownload.",
+    "exportFailed": "Export mislukt",
+    "exportFailedMessage": "Kon uw gegevens niet exporteren. Probeer het opnieuw.",
     "importTitle": "Gegevens importeren",
     "importTitle": "Gegevens importeren",
     "importWarning": "Importeren zal <bold>alle bestaande gegevens vervangen</bold> door de inhoud van het back-upbestand. Deze actie kan niet ongedaan worden gemaakt.",
     "importWarning": "Importeren zal <bold>alle bestaande gegevens vervangen</bold> door de inhoud van het back-upbestand. Deze actie kan niet ongedaan worden gemaakt.",
     "importFileHint": "Ondersteunt zowel .zip (v2) als .json (v1) back-upbestanden.",
     "importFileHint": "Ondersteunt zowel .zip (v2) als .json (v1) back-upbestanden.",
     "uploadRestore": "Uploaden en herstellen",
     "uploadRestore": "Uploaden en herstellen",
-    "importComplete": "Import voltooid", "importCompleteMessage": "Alle gegevens zijn hersteld uit het back-upbestand.",
+    "importComplete": "Import voltooid",
+    "importCompleteMessage": "Alle gegevens zijn hersteld uit het back-upbestand.",
     "importFailed": "Import mislukt",
     "importFailed": "Import mislukt",
     "importFromOther": "Importeren van andere diensten",
     "importFromOther": "Importeren van andere diensten",
     "importFromOtherDescription": "Migreer uw gegevens van een ander platform. Selecteer een dienst om te beginnen.",
     "importFromOtherDescription": "Migreer uw gegevens van een ander platform. Selecteer een dienst om te beginnen.",
@@ -431,10 +710,14 @@
     "lubelogStep3": "Klik op <bold>Make Backup</bold> om een zipbestand te downloaden",
     "lubelogStep3": "Klik op <bold>Make Backup</bold> om een zipbestand te downloaden",
     "lubelogStep4": "Upload het gedownloade zipbestand hieronder",
     "lubelogStep4": "Upload het gedownloade zipbestand hieronder",
     "lubelogAddNote": "Dit zal <bold>gegevens toevoegen naast</bold> uw bestaande records — niets wordt verwijderd.",
     "lubelogAddNote": "Dit zal <bold>gegevens toevoegen naast</bold> uw bestaande records — niets wordt verwijderd.",
-    "lubelogImported": "Wat wordt geïmporteerd:", "lubelogVehicles": "Voertuigen",
-    "lubelogServiceRecords": "Servicerecords met bijlagen", "lubelogNotes": "Notities",
-    "lubelogFileHint": "Bestanden tot 200MB ondersteund.", "lubelogImportComplete": "LubeLog-import voltooid",
-    "cancel": "Annuleren", "import": "Importeren",
+    "lubelogImported": "Wat wordt geïmporteerd:",
+    "lubelogVehicles": "Voertuigen",
+    "lubelogServiceRecords": "Servicerecords met bijlagen",
+    "lubelogNotes": "Notities",
+    "lubelogFileHint": "Bestanden tot 200MB ondersteund.",
+    "lubelogImportComplete": "LubeLog-import voltooid",
+    "cancel": "Annuleren",
+    "import": "Importeren",
     "invoiceNinjaTitle": "Importeren van Invoice Ninja",
     "invoiceNinjaTitle": "Importeren van Invoice Ninja",
     "invoiceNinjaExportSteps": "Hoe u uw Invoice Ninja-gegevens exporteert:",
     "invoiceNinjaExportSteps": "Hoe u uw Invoice Ninja-gegevens exporteert:",
     "invoiceNinjaStep1": "Open uw Invoice Ninja-instantie",
     "invoiceNinjaStep1": "Open uw Invoice Ninja-instantie",
@@ -443,9 +726,11 @@
     "invoiceNinjaStep4": "Upload het gedownloade zipbestand hieronder",
     "invoiceNinjaStep4": "Upload het gedownloade zipbestand hieronder",
     "invoiceNinjaAddNote": "Dit zal <bold>gegevens toevoegen naast</bold> uw bestaande records — niets wordt verwijderd.",
     "invoiceNinjaAddNote": "Dit zal <bold>gegevens toevoegen naast</bold> uw bestaande records — niets wordt verwijderd.",
     "invoiceNinjaImported": "Wat wordt geïmporteerd:",
     "invoiceNinjaImported": "Wat wordt geïmporteerd:",
-    "invoiceNinjaCustomers": "Klanten", "invoiceNinjaProducts": "Producten (inventaris)",
+    "invoiceNinjaCustomers": "Klanten",
+    "invoiceNinjaProducts": "Producten (inventaris)",
     "invoiceNinjaInvoices": "Facturen als servicerecords (onderdelen en arbeid)",
     "invoiceNinjaInvoices": "Facturen als servicerecords (onderdelen en arbeid)",
-    "invoiceNinjaDocuments": "Documenten en bijlagen", "invoiceNinjaPayments": "Betalingen",
+    "invoiceNinjaDocuments": "Documenten en bijlagen",
+    "invoiceNinjaPayments": "Betalingen",
     "invoiceNinjaFileHint": "Upload een Invoice Ninja zip-exportbestand. Bestanden tot 200MB ondersteund.",
     "invoiceNinjaFileHint": "Upload een Invoice Ninja zip-exportbestand. Bestanden tot 200MB ondersteund.",
     "invoiceNinjaImportComplete": "Invoice Ninja-import voltooid"
     "invoiceNinjaImportComplete": "Invoice Ninja-import voltooid"
   },
   },
@@ -453,17 +738,24 @@
     "title": "White-label licentie",
     "title": "White-label licentie",
     "description": "Activeer een white-label licentie om de Torqvoice-branding te verwijderen van facturen en de applicatie.",
     "description": "Activeer een white-label licentie om de Torqvoice-branding te verwijderen van facturen en de applicatie.",
     "purchaseWhiteLabel": "White-label kopen",
     "purchaseWhiteLabel": "White-label kopen",
-    "status": "Status:", "active": "Actief", "inactive": "Inactief",
+    "status": "Status:",
+    "active": "Actief",
+    "inactive": "Inactief",
     "lastChecked": "Laatst gecontroleerd: {date}",
     "lastChecked": "Laatst gecontroleerd: {date}",
-    "enterLicenseKey": "Licentiesleutel invoeren", "validate": "Valideren",
-    "validated": "Licentie succesvol gevalideerd", "invalid": "Ongeldige licentiesleutel",
+    "enterLicenseKey": "Licentiesleutel invoeren",
+    "validate": "Valideren",
+    "validated": "Licentie succesvol gevalideerd",
+    "invalid": "Ongeldige licentiesleutel",
     "failedValidate": "Licentie valideren mislukt",
     "failedValidate": "Licentie valideren mislukt",
     "keyHint": "Elke licentiesleutel is gekoppeld aan deze organisatie en kan niet worden gedeeld tussen meerdere organisaties."
     "keyHint": "Elke licentiesleutel is gekoppeld aan deze organisatie en kan niet worden gedeeld tussen meerdere organisaties."
   },
   },
   "about": {
   "about": {
     "title": "Over Torqvoice",
     "title": "Over Torqvoice",
     "description": "Torqvoice is uw complete oplossing voor voertuigservicebeheer. Volg servicehistorie, reparaties en meer.",
     "description": "Torqvoice is uw complete oplossing voor voertuigservicebeheer. Volg servicehistorie, reparaties en meer.",
-    "version": "Versie", "website": "Website", "documentation": "Documentatie", "changelog": "Changelog"
+    "version": "Versie",
+    "website": "Website",
+    "documentation": "Documentatie",
+    "changelog": "Changelog"
   },
   },
   "portal": {
   "portal": {
     "title": "Klantenportaal",
     "title": "Klantenportaal",

+ 4 - 0
messages/pl/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Kwota do zapłaty",
     "amountDue": "Kwota do zapłaty",
     "notes": "Uwagi",
     "notes": "Uwagi",
     "bankAccount": "Til Konto / Konto bankowe",
     "bankAccount": "Til Konto / Konto bankowe",
+    "paymentInformation": "Informacje o płatności",
+    "orgNumberLabel": "Nr org.",
+    "paymentTermsLabel": "Warunki płatności",
+    "dueDateLabel": "Termin płatności",
     "diagnosticNotes": "Uwagi diagnostyczne",
     "diagnosticNotes": "Uwagi diagnostyczne",
     "attachedDocuments": "Załączone dokumenty",
     "attachedDocuments": "Załączone dokumenty",
     "seeAppendedPages": "{name} (patrz dołączone strony)",
     "seeAppendedPages": "{name} (patrz dołączone strony)",

+ 215 - 48
messages/pl/settings.json

@@ -10,24 +10,78 @@
       "system": "System"
       "system": "System"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Firma", "description": "Dane firmy i marka" },
-      "subscription": { "title": "Subskrypcja", "description": "Plan i fakturowanie" },
-      "account": { "title": "Konto", "description": "Nazwa, e-mail i haslo" },
-      "team": { "title": "Zespol", "description": "Czlonkowie i role" },
-      "invoice": { "title": "Faktura", "description": "Uklad i pola faktury" },
-      "templates": { "title": "Szablony", "description": "Szablony faktur, wycen i inspekcji" },
-      "payment": { "title": "Platnosc", "description": "Warunki platnosci i konta" },
-      "currency": { "title": "Waluta", "description": "Domyslna waluta i podatki" },
-      "customFields": { "title": "Pola niestandardowe", "description": "Definiowanie wlasnych pol danych" },
-      "email": { "title": "E-mail", "description": "Dostawca e-mail i wysylka" },
-      "sms": { "title": "SMS", "description": "Dostawca SMS i wiadomosci" },
-      "customerPortal": { "title": "Portal klienta", "description": "Portal samoobslugowy klienta" },
-      "workshop": { "title": "Warsztat", "description": "Domyslne ustawienia technika i robocizny" },
-      "maintenance": { "title": "Konserwacja", "description": "Przewidywane interwaly serwisowe" },
-      "appearance": { "title": "Wyglad", "description": "Motyw i wyswietlanie" },
-      "data": { "title": "Dane", "description": "Eksport i import kopii zapasowej" },
-      "license": { "title": "Licencja", "description": "Licencja white-label" },
-      "about": { "title": "Informacje", "description": "Wersja i informacje" }
+      "company": {
+        "title": "Firma",
+        "description": "Dane firmy i marka"
+      },
+      "subscription": {
+        "title": "Subskrypcja",
+        "description": "Plan i fakturowanie"
+      },
+      "account": {
+        "title": "Konto",
+        "description": "Nazwa, e-mail i haslo"
+      },
+      "team": {
+        "title": "Zespol",
+        "description": "Czlonkowie i role"
+      },
+      "invoice": {
+        "title": "Faktury i wyceny",
+        "description": "Ustawienia faktur, uklad i pola niestandardowe"
+      },
+      "templates": {
+        "title": "Szablony",
+        "description": "Styl wizualny, kolory i czcionki"
+      },
+      "payment": {
+        "title": "Platnosc",
+        "description": "Warunki platnosci i konta"
+      },
+      "currency": {
+        "title": "Waluta",
+        "description": "Domyslna waluta i podatki"
+      },
+      "customFields": {
+        "title": "Pola niestandardowe",
+        "description": "Definiowanie wlasnych pol danych"
+      },
+      "email": {
+        "title": "E-mail",
+        "description": "Dostawca e-mail i wysylka"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Dostawca SMS i wiadomosci"
+      },
+      "customerPortal": {
+        "title": "Portal klienta",
+        "description": "Portal samoobslugowy klienta"
+      },
+      "workshop": {
+        "title": "Warsztat",
+        "description": "Domyslne ustawienia technika i robocizny"
+      },
+      "maintenance": {
+        "title": "Konserwacja",
+        "description": "Przewidywane interwaly serwisowe"
+      },
+      "appearance": {
+        "title": "Wyglad",
+        "description": "Motyw i wyswietlanie"
+      },
+      "data": {
+        "title": "Dane",
+        "description": "Eksport i import kopii zapasowej"
+      },
+      "license": {
+        "title": "Licencja",
+        "description": "Licencja white-label"
+      },
+      "about": {
+        "title": "Informacje",
+        "description": "Wersja i informacje"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Uklad faktury",
     "title": "Uklad faktury",
     "description": "Skonfiguruj, co pojawia sie na generowanych fakturach PDF.",
     "description": "Skonfiguruj, co pojawia sie na generowanych fakturach PDF.",
+    "tabs": {
+      "general": "Ogólne",
+      "layout": "Układ",
+      "customFields": "Pola niestandardowe"
+    },
     "invoiceNumberFormat": "Format numeru faktury",
     "invoiceNumberFormat": "Format numeru faktury",
     "invoiceNumberFormatHint": "Uzyj <code>{year}</code> dla biezacego roku. Podglad: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Uzyj <code>{year}</code> dla biezacego roku. Podglad: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Nastepny numer faktury",
     "nextInvoiceNumber": "Nastepny numer faktury",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Dziekujemy za zamowienie!",
     "footerPlaceholder": "Dziekujemy za zamowienie!",
     "footerHint": "Ten tekst pojawia sie na dole kazdej faktury",
     "footerHint": "Ten tekst pojawia sie na dole kazdej faktury",
     "saveInvoice": "Zapisz ustawienia faktury",
     "saveInvoice": "Zapisz ustawienia faktury",
-    "saved": "Ustawienia faktury zapisane"
+    "saved": "Ustawienia faktury zapisane",
+    "layoutDescription": "Dostosuj kolejność i widoczność sekcji w fakturach i wycenach.",
+    "layoutSaved": "Układ zapisany pomyślnie",
+    "preview": "Podgląd",
+    "layoutDocInvoice": "Faktura",
+    "layoutDocQuote": "Wycena",
+    "saveLayout": "Zapisz układ",
+    "resetLayout": "Resetuj"
+  },
+  "layoutEditor": {
+    "helpText": "Przeciągnij, aby zmienić kolejność. Użyj przełącznika szerokości, aby umieścić sekcje obok siebie. Kolejne sekcje o połowie szerokości (L/P) będą wyświetlane w dwóch kolumnach.",
+    "addField": "Dodaj pole",
+    "custom": "niestandardowe",
+    "full": "Pełna",
+    "L": "L",
+    "R": "P",
+    "left": "lewa",
+    "right": "prawa",
+    "halfWidthHint": "Połowa szerokości ({column}) — kliknij dla pełnej szerokości",
+    "fullWidthHint": "Pełna szerokość — kliknij dla połowy szerokości (lewa)",
+    "sections": {
+      "header": "Nagłówek",
+      "customer": "Klient",
+      "vehicle": "Pojazd",
+      "service": "Usługa",
+      "parts_table": "Części",
+      "labor_table": "Robocizna",
+      "totals": "Podsumowanie",
+      "notes": "Notatki",
+      "diagnostic_notes": "Notatki diagnostyczne",
+      "bank_account": "Konto bankowe",
+      "footer": "Stopka",
+      "general": "Ogólne"
+    },
+    "fields": {
+      "customer_name": "Nazwa klienta",
+      "customer_company": "Firma",
+      "customer_address": "Adres",
+      "customer_email": "E-mail",
+      "customer_phone": "Telefon",
+      "vehicle_name": "Pojazd",
+      "vin": "VIN",
+      "license_plate": "Tablica rejestracyjna",
+      "mileage": "Przebieg",
+      "service_title": "Tytuł usługi",
+      "service_type": "Typ usługi",
+      "tech_name": "Technik",
+      "logo": "Logo",
+      "company_name": "Nazwa firmy",
+      "bank_account": "Konto bankowe",
+      "org_number": "Numer organizacji"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Szablony",
     "title": "Szablony",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Styl naglowka",
     "headerStyle": "Styl naglowka",
+    "logoSize": "Rozmiar logo",
     "standard": "Standardowy",
     "standard": "Standardowy",
     "compact": "Kompaktowy",
     "compact": "Kompaktowy",
     "modern": "Nowoczesny",
     "modern": "Nowoczesny",
@@ -315,6 +426,12 @@
     "rate": "Stawka",
     "rate": "Stawka",
     "subtotal": "Suma czesciowa",
     "subtotal": "Suma czesciowa",
     "tax": "Podatek ({rate}%)",
     "tax": "Podatek ({rate}%)",
+    "previewService": "Serwis",
+    "previewNotes": "Uwagi",
+    "previewDiagnosticNotes": "Notatki diagnostyczne",
+    "previewDue": "Termin: {date}",
+    "previewAdditionalInfo": "Dodatkowe informacje",
+    "previewThankYou": "Dziękujemy za zaufanie",
     "saveInvoiceTemplate": "Zapisz szablon faktury",
     "saveInvoiceTemplate": "Zapisz szablon faktury",
     "saveQuotationTemplate": "Zapisz szablon wyceny",
     "saveQuotationTemplate": "Zapisz szablon wyceny",
     "invoiceTemplateSaved": "Ustawienia szablonu faktury zapisane",
     "invoiceTemplateSaved": "Ustawienia szablonu faktury zapisane",
@@ -413,14 +530,38 @@
     "saveSettings": "Zapisz ustawienia",
     "saveSettings": "Zapisz ustawienia",
     "saved": "Ustawienia zapisane",
     "saved": "Ustawienia zapisane",
     "currencies": {
     "currencies": {
-      "USD": "Dolar amerykanski", "EUR": "Euro", "GBP": "Funt brytyjski", "NOK": "Korona norweska",
-      "SEK": "Korona szwedzka", "DKK": "Korona dunska", "CHF": "Frank szwajcarski", "CAD": "Dolar kanadyjski",
-      "AUD": "Dolar australijski", "NZD": "Dolar nowozelandzki", "JPY": "Jen japonski", "CNY": "Juan chinski",
-      "INR": "Rupia indyjska", "BRL": "Real brazylijski", "MXN": "Peso meksykanskie", "PLN": "Zloty polski",
-      "CZK": "Korona czeska", "HUF": "Forint wegierski", "TRY": "Lira turecka", "ZAR": "Rand poludniowoafrykanski",
-      "KRW": "Won poludniowokoreanskii", "SGD": "Dolar singapurski", "HKD": "Dolar hongkonski", "THB": "Baht tajski",
-      "ISK": "Korona islandzka", "RON": "Lej rumunski", "ILS": "Szekel izraelski", "PHP": "Peso filipinskie",
-      "IDR": "Rupia indonezyjska", "MYR": "Ringgit malezyjski", "AED": "Dirham ZEA", "SAR": "Rial saudyjski"
+      "USD": "Dolar amerykanski",
+      "EUR": "Euro",
+      "GBP": "Funt brytyjski",
+      "NOK": "Korona norweska",
+      "SEK": "Korona szwedzka",
+      "DKK": "Korona dunska",
+      "CHF": "Frank szwajcarski",
+      "CAD": "Dolar kanadyjski",
+      "AUD": "Dolar australijski",
+      "NZD": "Dolar nowozelandzki",
+      "JPY": "Jen japonski",
+      "CNY": "Juan chinski",
+      "INR": "Rupia indyjska",
+      "BRL": "Real brazylijski",
+      "MXN": "Peso meksykanskie",
+      "PLN": "Zloty polski",
+      "CZK": "Korona czeska",
+      "HUF": "Forint wegierski",
+      "TRY": "Lira turecka",
+      "ZAR": "Rand poludniowoafrykanski",
+      "KRW": "Won poludniowokoreanskii",
+      "SGD": "Dolar singapurski",
+      "HKD": "Dolar hongkonski",
+      "THB": "Baht tajski",
+      "ISK": "Korona islandzka",
+      "RON": "Lej rumunski",
+      "ILS": "Szekel izraelski",
+      "PHP": "Peso filipinskie",
+      "IDR": "Rupia indonezyjska",
+      "MYR": "Ringgit malezyjski",
+      "AED": "Dirham ZEA",
+      "SAR": "Rial saudyjski"
     }
     }
   },
   },
   "customFields": {
   "customFields": {
@@ -486,7 +627,10 @@
     "imperial": "Imperialny (mile, galony)",
     "imperial": "Imperialny (mile, galony)",
     "distanceLabel": "Odleglosc",
     "distanceLabel": "Odleglosc",
     "volumeLabel": "Objetosc",
     "volumeLabel": "Objetosc",
-    "kilometers": "kilometry (km)", "liters": "litry (L)", "miles": "mile (mi)", "gallons": "galony (gal)",
+    "kilometers": "kilometry (km)",
+    "liters": "litry (L)",
+    "miles": "mile (mi)",
+    "gallons": "galony (gal)",
     "saveWorkshop": "Zapisz ustawienia warsztatu",
     "saveWorkshop": "Zapisz ustawienia warsztatu",
     "saved": "Ustawienia warsztatu zapisane"
     "saved": "Ustawienia warsztatu zapisane"
   },
   },
@@ -530,22 +674,32 @@
     "includeInBackup": "Dolacz do kopii zapasowej:",
     "includeInBackup": "Dolacz do kopii zapasowej:",
     "selectAll": "Zaznacz wszystko",
     "selectAll": "Zaznacz wszystko",
     "deselectAll": "Odznacz wszystko",
     "deselectAll": "Odznacz wszystko",
-    "optSettings": "Ustawienia", "optSettingsDesc": "Info warsztatu, konfiguracja faktur, dostawcy platnosci",
-    "optCustomers": "Klienci", "optCustomersDesc": "Rekordy klientow i dane kontaktowe",
-    "optVehicles": "Pojazdy i rekordy serwisowe", "optVehiclesDesc": "Pojazdy, notatki, dzienniki paliwa, przypomnienia, rekordy serwisowe",
-    "optQuotes": "Wyceny", "optQuotesDesc": "Wyceny z czesciami i robocizna",
-    "optInventory": "Inwentarz", "optInventoryDesc": "Czesci inwentarza i poziomy zapasow",
-    "optCustomFields": "Pola niestandardowe", "optCustomFieldsDesc": "Definicje i wartosci pol niestandardowych",
-    "optFiles": "Przeslane pliki", "optFilesDesc": "Zdjecia, zalaczniki serwisowe, logo",
+    "optSettings": "Ustawienia",
+    "optSettingsDesc": "Info warsztatu, konfiguracja faktur, dostawcy platnosci",
+    "optCustomers": "Klienci",
+    "optCustomersDesc": "Rekordy klientow i dane kontaktowe",
+    "optVehicles": "Pojazdy i rekordy serwisowe",
+    "optVehiclesDesc": "Pojazdy, notatki, dzienniki paliwa, przypomnienia, rekordy serwisowe",
+    "optQuotes": "Wyceny",
+    "optQuotesDesc": "Wyceny z czesciami i robocizna",
+    "optInventory": "Inwentarz",
+    "optInventoryDesc": "Czesci inwentarza i poziomy zapasow",
+    "optCustomFields": "Pola niestandardowe",
+    "optCustomFieldsDesc": "Definicje i wartosci pol niestandardowych",
+    "optFiles": "Przeslane pliki",
+    "optFilesDesc": "Zdjecia, zalaczniki serwisowe, logo",
     "filesWarning": "Zawiera przeslane pliki — kopia zapasowa moze byc wieksza",
     "filesWarning": "Zawiera przeslane pliki — kopia zapasowa moze byc wieksza",
     "downloadBackup": "Pobierz kopie zapasowa",
     "downloadBackup": "Pobierz kopie zapasowa",
-    "exportComplete": "Eksport zakonczony", "exportCompleteMessage": "Plik kopii zapasowej zostal pobrany.",
-    "exportFailed": "Eksport nie powiodl sie", "exportFailedMessage": "Nie udalo sie wyeksportowac danych. Sprobuj ponownie.",
+    "exportComplete": "Eksport zakonczony",
+    "exportCompleteMessage": "Plik kopii zapasowej zostal pobrany.",
+    "exportFailed": "Eksport nie powiodl sie",
+    "exportFailedMessage": "Nie udalo sie wyeksportowac danych. Sprobuj ponownie.",
     "importTitle": "Importuj dane",
     "importTitle": "Importuj dane",
     "importWarning": "Import <bold>zastapi wszystkie istniejace dane</bold> zawartoscia pliku kopii zapasowej. Tej akcji nie mozna cofnac.",
     "importWarning": "Import <bold>zastapi wszystkie istniejace dane</bold> zawartoscia pliku kopii zapasowej. Tej akcji nie mozna cofnac.",
     "importFileHint": "Obsluguje pliki kopii zapasowej .zip (v2) i .json (v1).",
     "importFileHint": "Obsluguje pliki kopii zapasowej .zip (v2) i .json (v1).",
     "uploadRestore": "Przeslij i przywroc",
     "uploadRestore": "Przeslij i przywroc",
-    "importComplete": "Import zakonczony", "importCompleteMessage": "Wszystkie dane zostaly przywrocone z pliku kopii zapasowej.",
+    "importComplete": "Import zakonczony",
+    "importCompleteMessage": "Wszystkie dane zostaly przywrocone z pliku kopii zapasowej.",
     "importFailed": "Import nie powiodl sie",
     "importFailed": "Import nie powiodl sie",
     "importFromOther": "Importuj z innych uslug",
     "importFromOther": "Importuj z innych uslug",
     "importFromOtherDescription": "Migruj dane z innej platformy. Wybierz usluge, aby rozpoczac.",
     "importFromOtherDescription": "Migruj dane z innej platformy. Wybierz usluge, aby rozpoczac.",
@@ -556,10 +710,14 @@
     "lubelogStep3": "Kliknij <bold>Make Backup</bold> aby pobrac plik zip",
     "lubelogStep3": "Kliknij <bold>Make Backup</bold> aby pobrac plik zip",
     "lubelogStep4": "Przeslij pobrany plik zip ponizej",
     "lubelogStep4": "Przeslij pobrany plik zip ponizej",
     "lubelogAddNote": "To <bold>doda dane obok</bold> istniejacych rekordow — nic nie zostanie usuniete.",
     "lubelogAddNote": "To <bold>doda dane obok</bold> istniejacych rekordow — nic nie zostanie usuniete.",
-    "lubelogImported": "Co zostanie zaimportowane:", "lubelogVehicles": "Pojazdy",
-    "lubelogServiceRecords": "Rekordy serwisowe z zalacznikami", "lubelogNotes": "Notatki",
-    "lubelogFileHint": "Obslugiwane pliki do 200MB.", "lubelogImportComplete": "Import z LubeLog zakonczony",
-    "cancel": "Anuluj", "import": "Importuj",
+    "lubelogImported": "Co zostanie zaimportowane:",
+    "lubelogVehicles": "Pojazdy",
+    "lubelogServiceRecords": "Rekordy serwisowe z zalacznikami",
+    "lubelogNotes": "Notatki",
+    "lubelogFileHint": "Obslugiwane pliki do 200MB.",
+    "lubelogImportComplete": "Import z LubeLog zakonczony",
+    "cancel": "Anuluj",
+    "import": "Importuj",
     "invoiceNinjaTitle": "Importuj z Invoice Ninja",
     "invoiceNinjaTitle": "Importuj z Invoice Ninja",
     "invoiceNinjaExportSteps": "Jak wyeksportowac dane z Invoice Ninja:",
     "invoiceNinjaExportSteps": "Jak wyeksportowac dane z Invoice Ninja:",
     "invoiceNinjaStep1": "Otworz instancje Invoice Ninja",
     "invoiceNinjaStep1": "Otworz instancje Invoice Ninja",
@@ -568,9 +726,11 @@
     "invoiceNinjaStep4": "Przeslij pobrany plik zip ponizej",
     "invoiceNinjaStep4": "Przeslij pobrany plik zip ponizej",
     "invoiceNinjaAddNote": "To <bold>doda dane obok</bold> istniejacych rekordow — nic nie zostanie usuniete.",
     "invoiceNinjaAddNote": "To <bold>doda dane obok</bold> istniejacych rekordow — nic nie zostanie usuniete.",
     "invoiceNinjaImported": "Co zostanie zaimportowane:",
     "invoiceNinjaImported": "Co zostanie zaimportowane:",
-    "invoiceNinjaCustomers": "Klienci", "invoiceNinjaProducts": "Produkty (inwentarz)",
+    "invoiceNinjaCustomers": "Klienci",
+    "invoiceNinjaProducts": "Produkty (inwentarz)",
     "invoiceNinjaInvoices": "Faktury jako rekordy serwisowe (czesci i robocizna)",
     "invoiceNinjaInvoices": "Faktury jako rekordy serwisowe (czesci i robocizna)",
-    "invoiceNinjaDocuments": "Dokumenty i zalaczniki", "invoiceNinjaPayments": "Platnosci",
+    "invoiceNinjaDocuments": "Dokumenty i zalaczniki",
+    "invoiceNinjaPayments": "Platnosci",
     "invoiceNinjaFileHint": "Przeslij plik eksportu zip Invoice Ninja. Obslugiwane pliki do 200MB.",
     "invoiceNinjaFileHint": "Przeslij plik eksportu zip Invoice Ninja. Obslugiwane pliki do 200MB.",
     "invoiceNinjaImportComplete": "Import z Invoice Ninja zakonczony"
     "invoiceNinjaImportComplete": "Import z Invoice Ninja zakonczony"
   },
   },
@@ -578,17 +738,24 @@
     "title": "Licencja white-label",
     "title": "Licencja white-label",
     "description": "Aktywuj licencje white-label, aby usunac marke Torqvoice z faktur i aplikacji.",
     "description": "Aktywuj licencje white-label, aby usunac marke Torqvoice z faktur i aplikacji.",
     "purchaseWhiteLabel": "Kup white-label",
     "purchaseWhiteLabel": "Kup white-label",
-    "status": "Status:", "active": "Aktywna", "inactive": "Nieaktywna",
+    "status": "Status:",
+    "active": "Aktywna",
+    "inactive": "Nieaktywna",
     "lastChecked": "Ostatnio sprawdzono: {date}",
     "lastChecked": "Ostatnio sprawdzono: {date}",
-    "enterLicenseKey": "Wprowadz klucz licencji", "validate": "Zweryfikuj",
-    "validated": "Licencja zweryfikowana pomyslnie", "invalid": "Nieprawidlowy klucz licencji",
+    "enterLicenseKey": "Wprowadz klucz licencji",
+    "validate": "Zweryfikuj",
+    "validated": "Licencja zweryfikowana pomyslnie",
+    "invalid": "Nieprawidlowy klucz licencji",
     "failedValidate": "Nie udalo sie zweryfikowac licencji",
     "failedValidate": "Nie udalo sie zweryfikowac licencji",
     "keyHint": "Kazdy klucz licencji jest powiazany z ta organizacja i nie moze byc wspoldzielony miedzy wieloma organizacjami."
     "keyHint": "Kazdy klucz licencji jest powiazany z ta organizacja i nie moze byc wspoldzielony miedzy wieloma organizacjami."
   },
   },
   "about": {
   "about": {
     "title": "O Torqvoice",
     "title": "O Torqvoice",
     "description": "Torqvoice to kompletne rozwiazanie do zarzadzania serwisem pojazdow. Sledz historie serwisowa, naprawy i wiecej.",
     "description": "Torqvoice to kompletne rozwiazanie do zarzadzania serwisem pojazdow. Sledz historie serwisowa, naprawy i wiecej.",
-    "version": "Wersja", "website": "Strona", "documentation": "Dokumentacja", "changelog": "Dziennik zmian"
+    "version": "Wersja",
+    "website": "Strona",
+    "documentation": "Dokumentacja",
+    "changelog": "Dziennik zmian"
   },
   },
   "portal": {
   "portal": {
     "title": "Portal klienta",
     "title": "Portal klienta",

+ 4 - 0
messages/pt-BR/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Valor a Pagar",
     "amountDue": "Valor a Pagar",
     "notes": "Observações",
     "notes": "Observações",
     "bankAccount": "Til Konto / Conta Bancária",
     "bankAccount": "Til Konto / Conta Bancária",
+    "paymentInformation": "Informações de pagamento",
+    "orgNumberLabel": "Nº da org.",
+    "paymentTermsLabel": "Condições de pagamento",
+    "dueDateLabel": "Data de vencimento",
     "diagnosticNotes": "Notas de Diagnóstico",
     "diagnosticNotes": "Notas de Diagnóstico",
     "attachedDocuments": "Documentos Anexados",
     "attachedDocuments": "Documentos Anexados",
     "seeAppendedPages": "{name} (ver páginas anexadas)",
     "seeAppendedPages": "{name} (ver páginas anexadas)",

+ 136 - 19
messages/pt-BR/settings.json

@@ -10,24 +10,78 @@
       "system": "Sistema"
       "system": "Sistema"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Empresa", "description": "Detalhes comerciais e marca" },
-      "subscription": { "title": "Assinatura", "description": "Plano e faturamento" },
-      "account": { "title": "Conta", "description": "Nome, e-mail e senha" },
-      "team": { "title": "Equipe", "description": "Membros e funcoes" },
-      "invoice": { "title": "Fatura", "description": "Layout e campos da fatura" },
-      "templates": { "title": "Modelos", "description": "Modelos de faturas, orcamentos e inspecoes" },
-      "payment": { "title": "Pagamento", "description": "Condicoes de pagamento e contas" },
-      "currency": { "title": "Moeda", "description": "Padroes de moeda e impostos" },
-      "customFields": { "title": "Campos personalizados", "description": "Definir campos de dados personalizados" },
-      "email": { "title": "E-mail", "description": "Provedor de e-mail e envio" },
-      "sms": { "title": "SMS", "description": "Provedor SMS e mensagens" },
-      "customerPortal": { "title": "Portal do cliente", "description": "Portal de autoatendimento do cliente" },
-      "workshop": { "title": "Oficina", "description": "Padroes de tecnico e mao de obra" },
-      "maintenance": { "title": "Manutencao", "description": "Intervalos de servico previstos" },
-      "appearance": { "title": "Aparencia", "description": "Tema e exibicao" },
-      "data": { "title": "Dados", "description": "Exportar e importar backup" },
-      "license": { "title": "Licenca", "description": "Licenca marca branca" },
-      "about": { "title": "Sobre", "description": "Versao e informacoes" }
+      "company": {
+        "title": "Empresa",
+        "description": "Detalhes comerciais e marca"
+      },
+      "subscription": {
+        "title": "Assinatura",
+        "description": "Plano e faturamento"
+      },
+      "account": {
+        "title": "Conta",
+        "description": "Nome, e-mail e senha"
+      },
+      "team": {
+        "title": "Equipe",
+        "description": "Membros e funcoes"
+      },
+      "invoice": {
+        "title": "Faturas e orcamentos",
+        "description": "Configuracoes de fatura, layout e campos personalizados"
+      },
+      "templates": {
+        "title": "Modelos",
+        "description": "Estilo visual, cores e fontes"
+      },
+      "payment": {
+        "title": "Pagamento",
+        "description": "Condicoes de pagamento e contas"
+      },
+      "currency": {
+        "title": "Moeda",
+        "description": "Padroes de moeda e impostos"
+      },
+      "customFields": {
+        "title": "Campos personalizados",
+        "description": "Definir campos de dados personalizados"
+      },
+      "email": {
+        "title": "E-mail",
+        "description": "Provedor de e-mail e envio"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Provedor SMS e mensagens"
+      },
+      "customerPortal": {
+        "title": "Portal do cliente",
+        "description": "Portal de autoatendimento do cliente"
+      },
+      "workshop": {
+        "title": "Oficina",
+        "description": "Padroes de tecnico e mao de obra"
+      },
+      "maintenance": {
+        "title": "Manutencao",
+        "description": "Intervalos de servico previstos"
+      },
+      "appearance": {
+        "title": "Aparencia",
+        "description": "Tema e exibicao"
+      },
+      "data": {
+        "title": "Dados",
+        "description": "Exportar e importar backup"
+      },
+      "license": {
+        "title": "Licenca",
+        "description": "Licenca marca branca"
+      },
+      "about": {
+        "title": "Sobre",
+        "description": "Versao e informacoes"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Layout da fatura",
     "title": "Layout da fatura",
     "description": "Configure o que aparece nas faturas PDF geradas.",
     "description": "Configure o que aparece nas faturas PDF geradas.",
+    "tabs": {
+      "general": "Geral",
+      "layout": "Layout",
+      "customFields": "Campos personalizados"
+    },
     "invoiceNumberFormat": "Formato do numero da fatura",
     "invoiceNumberFormat": "Formato do numero da fatura",
     "invoiceNumberFormatHint": "Use <code>{year}</code> para o ano atual. Visualizacao: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Use <code>{year}</code> para o ano atual. Visualizacao: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Proximo numero da fatura",
     "nextInvoiceNumber": "Proximo numero da fatura",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "Obrigado pela preferencia!",
     "footerPlaceholder": "Obrigado pela preferencia!",
     "footerHint": "Este texto aparece no final de cada fatura",
     "footerHint": "Este texto aparece no final de cada fatura",
     "saveInvoice": "Salvar configuracoes da fatura",
     "saveInvoice": "Salvar configuracoes da fatura",
-    "saved": "Configuracoes da fatura salvas"
+    "saved": "Configuracoes da fatura salvas",
+    "layoutDescription": "Personalize a ordem e visibilidade das seções em suas faturas e orçamentos.",
+    "layoutSaved": "Layout salvo com sucesso",
+    "preview": "Pré-visualização",
+    "layoutDocInvoice": "Fatura",
+    "layoutDocQuote": "Orçamento",
+    "saveLayout": "Salvar layout",
+    "resetLayout": "Redefinir"
+  },
+  "layoutEditor": {
+    "helpText": "Arraste para reordenar. Use o alternador de largura para posicionar seções lado a lado. Seções consecutivas de meia largura (E/D) serão exibidas em duas colunas.",
+    "addField": "Adicionar campo",
+    "custom": "personalizado",
+    "full": "Inteiro",
+    "L": "E",
+    "R": "D",
+    "left": "esquerda",
+    "right": "direita",
+    "halfWidthHint": "Meia largura ({column}) — clique para largura inteira",
+    "fullWidthHint": "Largura inteira — clique para meia largura (esquerda)",
+    "sections": {
+      "header": "Cabeçalho",
+      "customer": "Cliente",
+      "vehicle": "Veículo",
+      "service": "Serviço",
+      "parts_table": "Peças",
+      "labor_table": "Mão de obra",
+      "totals": "Totais",
+      "notes": "Notas",
+      "diagnostic_notes": "Notas de diagnóstico",
+      "bank_account": "Conta bancária",
+      "footer": "Rodapé",
+      "general": "Geral"
+    },
+    "fields": {
+      "customer_name": "Nome do cliente",
+      "customer_company": "Empresa",
+      "customer_address": "Endereço",
+      "customer_email": "E-mail",
+      "customer_phone": "Telefone",
+      "vehicle_name": "Veículo",
+      "vin": "VIN",
+      "license_plate": "Placa",
+      "mileage": "Quilometragem",
+      "service_title": "Título do serviço",
+      "service_type": "Tipo de serviço",
+      "tech_name": "Técnico",
+      "logo": "Logo",
+      "company_name": "Nome da empresa",
+      "bank_account": "Conta bancária",
+      "org_number": "Número da organização"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Modelos",
     "title": "Modelos",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Estilo do cabecalho",
     "headerStyle": "Estilo do cabecalho",
+    "logoSize": "Tamanho do logotipo",
     "standard": "Padrao",
     "standard": "Padrao",
     "compact": "Compacto",
     "compact": "Compacto",
     "modern": "Moderno",
     "modern": "Moderno",
@@ -315,6 +426,12 @@
     "rate": "Taxa",
     "rate": "Taxa",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
     "tax": "Imposto ({rate}%)",
     "tax": "Imposto ({rate}%)",
+    "previewService": "Serviço",
+    "previewNotes": "Observações",
+    "previewDiagnosticNotes": "Notas de diagnóstico",
+    "previewDue": "Vencimento: {date}",
+    "previewAdditionalInfo": "Informações adicionais",
+    "previewThankYou": "Obrigado pela preferência",
     "saveInvoiceTemplate": "Salvar modelo de fatura",
     "saveInvoiceTemplate": "Salvar modelo de fatura",
     "saveQuotationTemplate": "Salvar modelo de orcamento",
     "saveQuotationTemplate": "Salvar modelo de orcamento",
     "invoiceTemplateSaved": "Configuracoes do modelo de fatura salvas",
     "invoiceTemplateSaved": "Configuracoes do modelo de fatura salvas",

+ 4 - 0
messages/tr/pdf.json

@@ -31,6 +31,10 @@
     "amountDue": "Ödenecek Tutar",
     "amountDue": "Ödenecek Tutar",
     "notes": "Notlar",
     "notes": "Notlar",
     "bankAccount": "Til Konto / Banka Hesabı",
     "bankAccount": "Til Konto / Banka Hesabı",
+    "paymentInformation": "Ödeme bilgileri",
+    "orgNumberLabel": "Org. No.",
+    "paymentTermsLabel": "Ödeme koşulları",
+    "dueDateLabel": "Vade tarihi",
     "diagnosticNotes": "Tanı Notları",
     "diagnosticNotes": "Tanı Notları",
     "attachedDocuments": "Ekli Belgeler",
     "attachedDocuments": "Ekli Belgeler",
     "seeAppendedPages": "{name} (eklenen sayfalara bakınız)",
     "seeAppendedPages": "{name} (eklenen sayfalara bakınız)",

+ 136 - 19
messages/tr/settings.json

@@ -10,24 +10,78 @@
       "system": "Sistem"
       "system": "Sistem"
     },
     },
     "items": {
     "items": {
-      "company": { "title": "Şirket", "description": "İşletme bilgileri & marka" },
-      "subscription": { "title": "Abonelik", "description": "Plan & faturalandırma" },
-      "account": { "title": "Hesap", "description": "Ad, e-posta & şifre" },
-      "team": { "title": "Ekip", "description": "Üyeler & roller" },
-      "invoice": { "title": "Fatura", "description": "Fatura düzeni & alanlar" },
-      "templates": { "title": "Şablonlar", "description": "Fatura, teklif & muayene şablonları" },
-      "payment": { "title": "Ödeme", "description": "Ödeme koşulları & hesaplar" },
-      "currency": { "title": "Para Birimi", "description": "Para birimi & vergi varsayılanları" },
-      "customFields": { "title": "Özel Alanlar", "description": "Özel veri alanları tanımlayın" },
-      "email": { "title": "E-posta", "description": "E-posta sağlayıcı & gönderim" },
-      "sms": { "title": "SMS", "description": "SMS sağlayıcı & mesajlaşma" },
-      "customerPortal": { "title": "Müşteri Portalı", "description": "Müşteri self-servis portalı" },
-      "workshop": { "title": "Atölye", "description": "Teknisyen & işçilik varsayılanları" },
-      "maintenance": { "title": "Bakım", "description": "Öngörülen servis aralıkları" },
-      "appearance": { "title": "Görünüm", "description": "Tema & görüntüleme" },
-      "data": { "title": "Veri", "description": "Dışa & içe aktarma yedeği" },
-      "license": { "title": "Lisans", "description": "Beyaz etiket lisansı" },
-      "about": { "title": "Hakkında", "description": "Sürüm & bilgi" }
+      "company": {
+        "title": "Şirket",
+        "description": "İşletme bilgileri & marka"
+      },
+      "subscription": {
+        "title": "Abonelik",
+        "description": "Plan & faturalandırma"
+      },
+      "account": {
+        "title": "Hesap",
+        "description": "Ad, e-posta & şifre"
+      },
+      "team": {
+        "title": "Ekip",
+        "description": "Üyeler & roller"
+      },
+      "invoice": {
+        "title": "Faturalar ve teklifler",
+        "description": "Fatura ayarlari, duzen ve ozel alanlar"
+      },
+      "templates": {
+        "title": "Şablonlar",
+        "description": "Gorsel stil, renkler ve yazi tipleri"
+      },
+      "payment": {
+        "title": "Ödeme",
+        "description": "Ödeme koşulları & hesaplar"
+      },
+      "currency": {
+        "title": "Para Birimi",
+        "description": "Para birimi & vergi varsayılanları"
+      },
+      "customFields": {
+        "title": "Özel Alanlar",
+        "description": "Özel veri alanları tanımlayın"
+      },
+      "email": {
+        "title": "E-posta",
+        "description": "E-posta sağlayıcı & gönderim"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "SMS sağlayıcı & mesajlaşma"
+      },
+      "customerPortal": {
+        "title": "Müşteri Portalı",
+        "description": "Müşteri self-servis portalı"
+      },
+      "workshop": {
+        "title": "Atölye",
+        "description": "Teknisyen & işçilik varsayılanları"
+      },
+      "maintenance": {
+        "title": "Bakım",
+        "description": "Öngörülen servis aralıkları"
+      },
+      "appearance": {
+        "title": "Görünüm",
+        "description": "Tema & görüntüleme"
+      },
+      "data": {
+        "title": "Veri",
+        "description": "Dışa & içe aktarma yedeği"
+      },
+      "license": {
+        "title": "Lisans",
+        "description": "Beyaz etiket lisansı"
+      },
+      "about": {
+        "title": "Hakkında",
+        "description": "Sürüm & bilgi"
+      }
     },
     },
     "pro": "PRO"
     "pro": "PRO"
   },
   },
@@ -258,6 +312,11 @@
   "invoice": {
   "invoice": {
     "title": "Fatura Düzeni",
     "title": "Fatura Düzeni",
     "description": "Oluşturulan PDF faturalarda nelerin görüneceğini yapılandırın.",
     "description": "Oluşturulan PDF faturalarda nelerin görüneceğini yapılandırın.",
+    "tabs": {
+      "general": "Genel",
+      "layout": "Düzen",
+      "customFields": "Özel alanlar"
+    },
     "invoiceNumberFormat": "Fatura Numara Formatı",
     "invoiceNumberFormat": "Fatura Numara Formatı",
     "invoiceNumberFormatHint": "Geçerli yıl için <code>{year}</code> kullanın. Önizleme: <bold>{preview}</bold>",
     "invoiceNumberFormatHint": "Geçerli yıl için <code>{year}</code> kullanın. Önizleme: <bold>{preview}</bold>",
     "nextInvoiceNumber": "Sonraki Fatura Numarası",
     "nextInvoiceNumber": "Sonraki Fatura Numarası",
@@ -278,7 +337,58 @@
     "footerPlaceholder": "İşiniz için teşekkür ederiz!",
     "footerPlaceholder": "İşiniz için teşekkür ederiz!",
     "footerHint": "Bu metin her faturanın altında görünür",
     "footerHint": "Bu metin her faturanın altında görünür",
     "saveInvoice": "Fatura Ayarlarını Kaydet",
     "saveInvoice": "Fatura Ayarlarını Kaydet",
-    "saved": "Fatura ayarları kaydedildi"
+    "saved": "Fatura ayarları kaydedildi",
+    "layoutDescription": "Fatura ve tekliflerinizdeki bölümlerin sırasını ve görünürlüğünü özelleştirin.",
+    "layoutSaved": "Düzen başarıyla kaydedildi",
+    "preview": "Önizleme",
+    "layoutDocInvoice": "Fatura",
+    "layoutDocQuote": "Teklif",
+    "saveLayout": "Düzeni kaydet",
+    "resetLayout": "Sıfırla"
+  },
+  "layoutEditor": {
+    "helpText": "Sıralamak için sürükleyin. Bölümleri yan yana yerleştirmek için genişlik değiştiriciyi kullanın. Ardışık yarım genişlik bölümleri (S/S) iki sütun halinde görüntülenir.",
+    "addField": "Alan ekle",
+    "custom": "özel",
+    "full": "Tam",
+    "L": "S",
+    "R": "S",
+    "left": "sol",
+    "right": "sağ",
+    "halfWidthHint": "Yarım genişlik ({column}) — tam genişlik için tıklayın",
+    "fullWidthHint": "Tam genişlik — yarım genişlik için tıklayın (sol)",
+    "sections": {
+      "header": "Başlık",
+      "customer": "Müşteri",
+      "vehicle": "Araç",
+      "service": "Servis",
+      "parts_table": "Parçalar",
+      "labor_table": "İşçilik",
+      "totals": "Toplamlar",
+      "notes": "Notlar",
+      "diagnostic_notes": "Teşhis notları",
+      "bank_account": "Banka hesabı",
+      "footer": "Alt bilgi",
+      "general": "Genel"
+    },
+    "fields": {
+      "customer_name": "Müşteri adı",
+      "customer_company": "Şirket",
+      "customer_address": "Adres",
+      "customer_email": "E-posta",
+      "customer_phone": "Telefon",
+      "vehicle_name": "Araç",
+      "vin": "VIN",
+      "license_plate": "Plaka",
+      "mileage": "Kilometre",
+      "service_title": "Servis başlığı",
+      "service_type": "Servis türü",
+      "tech_name": "Teknisyen",
+      "logo": "Logo",
+      "company_name": "Şirket adı",
+      "bank_account": "Banka hesabı",
+      "org_number": "Kuruluş numarası"
+    }
   },
   },
   "templates": {
   "templates": {
     "title": "Şablonlar",
     "title": "Şablonlar",
@@ -299,6 +409,7 @@
     "timesRoman": "Times Roman",
     "timesRoman": "Times Roman",
     "courier": "Courier",
     "courier": "Courier",
     "headerStyle": "Başlık Stili",
     "headerStyle": "Başlık Stili",
+    "logoSize": "Logo boyutu",
     "standard": "Standart",
     "standard": "Standart",
     "compact": "Kompakt",
     "compact": "Kompakt",
     "modern": "Modern",
     "modern": "Modern",
@@ -315,6 +426,12 @@
     "rate": "Ücret",
     "rate": "Ücret",
     "subtotal": "Ara Toplam",
     "subtotal": "Ara Toplam",
     "tax": "Vergi (%{rate})",
     "tax": "Vergi (%{rate})",
+    "previewService": "Servis",
+    "previewNotes": "Notlar",
+    "previewDiagnosticNotes": "Teşhis notları",
+    "previewDue": "Vade: {date}",
+    "previewAdditionalInfo": "Ek bilgiler",
+    "previewThankYou": "Bizi tercih ettiğiniz için teşekkürler",
     "saveInvoiceTemplate": "Fatura Şablonunu Kaydet",
     "saveInvoiceTemplate": "Fatura Şablonunu Kaydet",
     "saveQuotationTemplate": "Teklif Şablonunu Kaydet",
     "saveQuotationTemplate": "Teklif Şablonunu Kaydet",
     "invoiceTemplateSaved": "Fatura şablon ayarları kaydedildi",
     "invoiceTemplateSaved": "Fatura şablon ayarları kaydedildi",

+ 15 - 0
package-lock.json

@@ -12,6 +12,7 @@
         "@aws-sdk/client-ses": "^3.992.0",
         "@aws-sdk/client-ses": "^3.992.0",
         "@better-auth/passkey": "^1.4.19",
         "@better-auth/passkey": "^1.4.19",
         "@dnd-kit/core": "^6.3.1",
         "@dnd-kit/core": "^6.3.1",
+        "@dnd-kit/sortable": "^10.0.0",
         "@dnd-kit/utilities": "^3.2.2",
         "@dnd-kit/utilities": "^3.2.2",
         "@hookform/resolvers": "^5.2.2",
         "@hookform/resolvers": "^5.2.2",
         "@prisma/client": "^6.19.2",
         "@prisma/client": "^6.19.2",
@@ -1809,6 +1810,20 @@
         "react-dom": ">=16.8.0"
         "react-dom": ">=16.8.0"
       }
       }
     },
     },
+    "node_modules/@dnd-kit/sortable": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+      "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+      "license": "MIT",
+      "dependencies": {
+        "@dnd-kit/utilities": "^3.2.2",
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "@dnd-kit/core": "^6.3.0",
+        "react": ">=16.8.0"
+      }
+    },
     "node_modules/@dnd-kit/utilities": {
     "node_modules/@dnd-kit/utilities": {
       "version": "3.2.2",
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
       "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "@aws-sdk/client-ses": "^3.992.0",
     "@aws-sdk/client-ses": "^3.992.0",
     "@better-auth/passkey": "^1.4.19",
     "@better-auth/passkey": "^1.4.19",
     "@dnd-kit/core": "^6.3.1",
     "@dnd-kit/core": "^6.3.1",
+    "@dnd-kit/sortable": "^10.0.0",
     "@dnd-kit/utilities": "^3.2.2",
     "@dnd-kit/utilities": "^3.2.2",
     "@hookform/resolvers": "^5.2.2",
     "@hookform/resolvers": "^5.2.2",
     "@prisma/client": "^6.19.2",
     "@prisma/client": "^6.19.2",

+ 1 - 3
src/__tests__/features/auth/passkey-signin.test.tsx

@@ -32,10 +32,8 @@ vi.mock("@/components/ui/button", () => ({
   Button: ({
   Button: ({
     children,
     children,
     ...props
     ...props
-  }: React.HTMLAttributes<HTMLButtonElement> & {
+  }: React.ButtonHTMLAttributes<HTMLButtonElement> & {
     variant?: string;
     variant?: string;
-    disabled?: boolean;
-    type?: string;
   }) => <button {...props}>{children}</button>,
   }) => <button {...props}>{children}</button>,
 }));
 }));
 vi.mock("@/components/ui/input", () => ({
 vi.mock("@/components/ui/input", () => ({

+ 2 - 2
src/__tests__/features/public/invoice-view.test.tsx

@@ -269,7 +269,7 @@ describe("InvoiceView", () => {
           expect.objectContaining({ method: "POST" })
           expect.objectContaining({ method: "POST" })
         );
         );
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/share/invoice/org-1/tok-xyz/checkout");
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/share/invoice/org-1/tok-xyz/checkout");
-        const body = JSON.parse(call[1].body);
+        const body = JSON.parse(call![1].body);
         expect(body.provider).toBe("stripe");
         expect(body.provider).toBe("stripe");
         expect(body.amount).toBe(200);
         expect(body.amount).toBe(200);
       });
       });
@@ -329,7 +329,7 @@ describe("InvoiceView", () => {
       await waitFor(() => {
       await waitFor(() => {
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/share/invoice/org-1/tok-xyz/verify");
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/share/invoice/org-1/tok-xyz/verify");
         expect(call).toBeTruthy();
         expect(call).toBeTruthy();
-        const body = JSON.parse(call[1].body);
+        const body = JSON.parse(call![1].body);
         expect(body.provider).toBe("vipps");
         expect(body.provider).toBe("vipps");
         expect(body.externalId).toBe("vipps-ref-456");
         expect(body.externalId).toBe("vipps-ref-456");
       });
       });

+ 2 - 2
src/__tests__/features/public/quote-view.test.tsx

@@ -245,7 +245,7 @@ describe("QuoteView", () => {
           expect.objectContaining({ method: "POST" })
           expect.objectContaining({ method: "POST" })
         );
         );
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/forms/quote-response");
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/forms/quote-response");
-        const body = JSON.parse(call[1].body);
+        const body = JSON.parse(call![1].body);
         expect(body.action).toBe("accepted");
         expect(body.action).toBe("accepted");
         expect(body.quoteId).toBe("quote-1");
         expect(body.quoteId).toBe("quote-1");
         expect(body.publicToken).toBe("tok-abc");
         expect(body.publicToken).toBe("tok-abc");
@@ -312,7 +312,7 @@ describe("QuoteView", () => {
       await waitFor(() => {
       await waitFor(() => {
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/forms/quote-response");
         const call = mockFetch.mock.calls.find((c: unknown[]) => c[0] === "/api/public/forms/quote-response");
         expect(call).toBeTruthy();
         expect(call).toBeTruthy();
-        const body = JSON.parse(call[1].body);
+        const body = JSON.parse(call![1].body);
         expect(body.action).toBe("changes_requested");
         expect(body.action).toBe("changes_requested");
         expect(body.message).toBe("Need a discount");
         expect(body.message).toBe("Need a discount");
       });
       });

+ 302 - 147
src/app/(authenticated)/settings/invoice/invoice-settings.tsx

@@ -1,13 +1,12 @@
 "use client";
 "use client";
 
 
-import { useState } from "react";
-import { useRouter } from "next/navigation";
+import { useState, useCallback } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
-import { Switch } from "@/components/ui/switch";
 import { Separator } from "@/components/ui/separator";
 import { Separator } from "@/components/ui/separator";
 import { Textarea } from "@/components/ui/textarea";
 import { Textarea } from "@/components/ui/textarea";
 import { toast } from "sonner";
 import { toast } from "sonner";
@@ -15,31 +14,88 @@ import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { FileText, Loader2, Save } from "lucide-react";
 import { FileText, Loader2, Save } from "lucide-react";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
+import { cn } from "@/lib/utils";
+import { InvoiceLayoutEditor } from "@/features/settings/Components/InvoiceLayoutEditor";
+import { InvoiceLayoutPreview } from "@/features/settings/Components/InvoiceLayoutPreview";
+import { saveInvoiceLayoutConfig, saveQuoteLayoutConfig } from "@/features/settings/Actions/invoiceLayoutActions";
+import { type InvoiceLayoutConfig, getDefaultInvoiceLayout } from "@/features/settings/Schema/invoiceLayoutSchema";
+import { CustomFieldsManager } from "@/features/custom-fields/Components/CustomFieldsManager";
 
 
-export function InvoiceSettings({ settings }: { settings: Record<string, string> }) {
+type TabType = "general" | "layout" | "customFields";
+
+interface FieldDef {
+  id: string;
+  name: string;
+  label: string;
+  fieldType: string;
+  entityType: string;
+  options: string | null;
+  required: boolean;
+  sortOrder: number;
+  isActive: boolean;
+}
+
+interface InvoiceSettingsProps {
+  settings: Record<string, string>;
+  initialInvoiceLayout?: InvoiceLayoutConfig;
+  initialQuoteLayout?: InvoiceLayoutConfig;
+  customFields: FieldDef[];
+  customFieldsEnabled: boolean;
+}
+
+export function InvoiceSettings({
+  settings,
+  initialInvoiceLayout,
+  initialQuoteLayout,
+  customFields,
+  customFieldsEnabled,
+}: InvoiceSettingsProps) {
   const router = useRouter();
   const router = useRouter();
+  const searchParams = useSearchParams();
   const t = useTranslations('settings');
   const t = useTranslations('settings');
   const [saving, setSaving] = useState(false);
   const [saving, setSaving] = useState(false);
 
 
+  const tab = (searchParams.get("tab") as TabType) || "general";
+  const setTab = useCallback((newTab: TabType) => {
+    const params = new URLSearchParams(searchParams.toString());
+    if (newTab === "general") {
+      params.delete("tab");
+    } else {
+      params.set("tab", newTab);
+    }
+    const qs = params.toString();
+    router.replace(`/settings/invoice${qs ? `?${qs}` : ""}`, { scroll: false });
+  }, [router, searchParams]);
+
+  // General tab state
   const [invoicePrefix, setInvoicePrefix] = useState(settings[SETTING_KEYS.INVOICE_PREFIX] || "{year}-");
   const [invoicePrefix, setInvoicePrefix] = useState(settings[SETTING_KEYS.INVOICE_PREFIX] || "{year}-");
   const [invoiceStartNumber, setInvoiceStartNumber] = useState(settings[SETTING_KEYS.INVOICE_START_NUMBER] || "");
   const [invoiceStartNumber, setInvoiceStartNumber] = useState(settings[SETTING_KEYS.INVOICE_START_NUMBER] || "");
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || "14");
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || "14");
-  const [showBankAccount, setShowBankAccount] = useState(settings[SETTING_KEYS.INVOICE_SHOW_BANK_ACCOUNT] === "true");
-  const [showOrgNumber, setShowOrgNumber] = useState(settings[SETTING_KEYS.INVOICE_SHOW_ORG_NUMBER] === "true");
-  const [showLogo, setShowLogo] = useState(settings[SETTING_KEYS.INVOICE_SHOW_LOGO] !== "false");
-  const [showCompanyName, setShowCompanyName] = useState(settings[SETTING_KEYS.INVOICE_SHOW_COMPANY_NAME] !== "false");
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || "");
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || "");
 
 
-  const handleSave = async () => {
+  // Layout tab state
+  const [invoiceLayout, setInvoiceLayout] = useState(initialInvoiceLayout ?? getDefaultInvoiceLayout());
+  const [quoteLayout, setQuoteLayout] = useState(initialQuoteLayout ?? getDefaultInvoiceLayout());
+  const [layoutDocType, setLayoutDocType] = useState<"invoice" | "quote">("invoice");
+
+  // Template values from saved settings
+  const invoiceTemplate = {
+    primaryColor: settings[SETTING_KEYS.INVOICE_PRIMARY_COLOR] || "#d97706",
+    fontFamily: settings[SETTING_KEYS.INVOICE_FONT_FAMILY] || "Helvetica",
+    headerStyle: settings[SETTING_KEYS.INVOICE_HEADER_STYLE] || "standard",
+  };
+  const quoteTemplate = {
+    primaryColor: settings[SETTING_KEYS.QUOTE_PRIMARY_COLOR] || invoiceTemplate.primaryColor,
+    fontFamily: settings[SETTING_KEYS.QUOTE_FONT_FAMILY] || invoiceTemplate.fontFamily,
+    headerStyle: settings[SETTING_KEYS.QUOTE_HEADER_STYLE] || invoiceTemplate.headerStyle,
+  };
+
+  const handleSaveGeneral = async () => {
     setSaving(true);
     setSaving(true);
     await setSettings({
     await setSettings({
       [SETTING_KEYS.INVOICE_PREFIX]: invoicePrefix,
       [SETTING_KEYS.INVOICE_PREFIX]: invoicePrefix,
       [SETTING_KEYS.INVOICE_START_NUMBER]: invoiceStartNumber,
       [SETTING_KEYS.INVOICE_START_NUMBER]: invoiceStartNumber,
       [SETTING_KEYS.INVOICE_DUE_DAYS]: dueDays,
       [SETTING_KEYS.INVOICE_DUE_DAYS]: dueDays,
-      [SETTING_KEYS.INVOICE_SHOW_BANK_ACCOUNT]: showBankAccount ? "true" : "false",
-      [SETTING_KEYS.INVOICE_SHOW_ORG_NUMBER]: showOrgNumber ? "true" : "false",
-      [SETTING_KEYS.INVOICE_SHOW_LOGO]: showLogo ? "true" : "false",
-      [SETTING_KEYS.INVOICE_SHOW_COMPANY_NAME]: showCompanyName ? "true" : "false",
       [SETTING_KEYS.INVOICE_FOOTER_NOTE]: footerNote,
       [SETTING_KEYS.INVOICE_FOOTER_NOTE]: footerNote,
     });
     });
     setSaving(false);
     setSaving(false);
@@ -47,156 +103,255 @@ export function InvoiceSettings({ settings }: { settings: Record<string, string>
     toast.success(t('invoice.saved'));
     toast.success(t('invoice.saved'));
   };
   };
 
 
+  const handleSaveLayout = async () => {
+    setSaving(true);
+    try {
+      if (layoutDocType === "invoice") {
+        await saveInvoiceLayoutConfig(invoiceLayout);
+      } else {
+        await saveQuoteLayoutConfig(quoteLayout);
+      }
+      toast.success(t('invoice.layoutSaved'));
+    } catch {
+      toast.error(t('templates.failedSave'));
+    }
+    setSaving(false);
+  };
+
   return (
   return (
     <div className="space-y-6">
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyBanner />
-      <ReadOnlyWrapper>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <FileText className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('invoice.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
-          <p className="text-sm text-muted-foreground">
-            {t('invoice.description')}
-          </p>
-
-          <div className="grid gap-4 sm:grid-cols-2">
-            <div className="space-y-2">
-              <Label htmlFor="invoicePrefix">{t('invoice.invoiceNumberFormat')}</Label>
-              <Input
-                id="invoicePrefix"
-                placeholder="{year}-"
-                value={invoicePrefix}
-                onChange={(e) => setInvoicePrefix(e.target.value)}
-              />
-              <p className="text-xs text-muted-foreground">
-                {t.rich('invoice.invoiceNumberFormatHint', {
-                  code: (chunks) => <code className="rounded bg-muted px-1">{chunks}</code>,
-                  bold: (chunks) => <span className="font-medium">{chunks}</span>,
-                  year: '{year}',
-                  preview: invoicePrefix.replace(/\{year\}/g, String(new Date().getFullYear())) + (invoiceStartNumber || '1001'),
-                })}
-              </p>
-            </div>
-            <div className="space-y-2">
-              <Label htmlFor="invoiceStartNumber">{t('invoice.nextInvoiceNumber')}</Label>
-              <Input
-                id="invoiceStartNumber"
-                type="number"
-                min="1"
-                placeholder={t('invoice.nextInvoiceNumberPlaceholder')}
-                value={invoiceStartNumber}
-                onChange={(e) => setInvoiceStartNumber(e.target.value)}
-                className="w-32"
-              />
-              <p className="text-xs text-muted-foreground">
-                {t('invoice.nextInvoiceNumberHint', { example: invoicePrefix + (invoiceStartNumber || '...') })}
-              </p>
-            </div>
-            <div className="space-y-2">
-              <Label htmlFor="dueDays">{t('invoice.dueDays')}</Label>
-              <Input
-                id="dueDays"
-                type="number"
-                min="0"
-                placeholder="14"
-                value={dueDays}
-                onChange={(e) => setDueDays(e.target.value)}
-                className="w-24"
-              />
-              <p className="text-xs text-muted-foreground">
-                {t('invoice.dueDaysHint')}
-              </p>
-            </div>
-          </div>
+      <div>
+        <h2 className="text-lg font-semibold">{t('invoice.title')}</h2>
+        <p className="text-sm text-muted-foreground">
+          {tab === "layout"
+            ? t('invoice.layoutDescription')
+            : tab === "customFields"
+              ? t('customFields.description')
+              : t('invoice.description')}
+        </p>
+      </div>
 
 
-          <Separator />
+      {/* Tab Buttons */}
+      <div className="flex gap-1 rounded-lg border bg-muted p-1">
+        <button
+          type="button"
+          onClick={() => setTab("general")}
+          className={cn(
+            "flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors",
+            tab === "general"
+              ? "bg-background text-foreground shadow-sm"
+              : "text-muted-foreground hover:text-foreground"
+          )}
+        >
+          {t('invoice.tabs.general')}
+        </button>
+        <button
+          type="button"
+          onClick={() => setTab("layout")}
+          className={cn(
+            "flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors",
+            tab === "layout"
+              ? "bg-background text-foreground shadow-sm"
+              : "text-muted-foreground hover:text-foreground"
+          )}
+        >
+          {t('invoice.tabs.layout')}
+        </button>
+        {customFieldsEnabled && (
+          <button
+            type="button"
+            onClick={() => setTab("customFields")}
+            className={cn(
+              "flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors",
+              tab === "customFields"
+                ? "bg-background text-foreground shadow-sm"
+                : "text-muted-foreground hover:text-foreground"
+            )}
+          >
+            {t('invoice.tabs.customFields')}
+          </button>
+        )}
+      </div>
 
 
-          <div className="space-y-4">
-            <h3 className="text-sm font-medium">{t('invoice.visibilityTitle')}</h3>
-            <div className="flex items-center justify-between rounded-lg border p-3">
-              <div>
-                <Label className="text-sm font-medium">{t('invoice.companyLogo')}</Label>
-                <p className="text-xs text-muted-foreground">
-                  {t('invoice.companyLogoHint')}
-                </p>
+      {tab === "general" ? (
+        <ReadOnlyWrapper>
+          <Card className="border-0 shadow-sm">
+            <CardHeader className="flex flex-row items-center gap-3 pb-4">
+              <FileText className="h-5 w-5 text-muted-foreground" />
+              <CardTitle className="text-lg">{t('invoice.tabs.general')}</CardTitle>
+            </CardHeader>
+            <CardContent className="space-y-6">
+              <div className="grid gap-4 sm:grid-cols-2">
+                <div className="space-y-2">
+                  <Label htmlFor="invoicePrefix">{t('invoice.invoiceNumberFormat')}</Label>
+                  <Input
+                    id="invoicePrefix"
+                    placeholder="{year}-"
+                    value={invoicePrefix}
+                    onChange={(e) => setInvoicePrefix(e.target.value)}
+                  />
+                  <p className="text-xs text-muted-foreground">
+                    {t.rich('invoice.invoiceNumberFormatHint', {
+                      code: (chunks) => <code className="rounded bg-muted px-1">{chunks}</code>,
+                      bold: (chunks) => <span className="font-medium">{chunks}</span>,
+                      year: '{year}',
+                      preview: invoicePrefix.replace(/\{year\}/g, String(new Date().getFullYear())) + (invoiceStartNumber || '1001'),
+                    })}
+                  </p>
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="invoiceStartNumber">{t('invoice.nextInvoiceNumber')}</Label>
+                  <Input
+                    id="invoiceStartNumber"
+                    type="number"
+                    min="1"
+                    placeholder={t('invoice.nextInvoiceNumberPlaceholder')}
+                    value={invoiceStartNumber}
+                    onChange={(e) => setInvoiceStartNumber(e.target.value)}
+                    className="w-32"
+                  />
+                  <p className="text-xs text-muted-foreground">
+                    {t('invoice.nextInvoiceNumberHint', { example: invoicePrefix + (invoiceStartNumber || '...') })}
+                  </p>
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="dueDays">{t('invoice.dueDays')}</Label>
+                  <Input
+                    id="dueDays"
+                    type="number"
+                    min="0"
+                    placeholder="14"
+                    value={dueDays}
+                    onChange={(e) => setDueDays(e.target.value)}
+                    className="w-24"
+                  />
+                  <p className="text-xs text-muted-foreground">
+                    {t('invoice.dueDaysHint')}
+                  </p>
+                </div>
               </div>
               </div>
-              <Switch
-                checked={showLogo}
-                onCheckedChange={setShowLogo}
-              />
-            </div>
-            <div className="flex items-center justify-between rounded-lg border p-3">
-              <div>
-                <Label className="text-sm font-medium">{t('invoice.companyName')}</Label>
+
+              <Separator />
+
+              <div className="space-y-2">
+                <Label htmlFor="footerNote">{t('invoice.customFooter')}</Label>
+                <Textarea
+                  id="footerNote"
+                  placeholder={t('invoice.footerPlaceholder')}
+                  rows={2}
+                  value={footerNote}
+                  onChange={(e) => setFooterNote(e.target.value)}
+                />
                 <p className="text-xs text-muted-foreground">
                 <p className="text-xs text-muted-foreground">
-                  {t('invoice.companyNameHint')}
+                  {t('invoice.footerHint')}
                 </p>
                 </p>
               </div>
               </div>
-              <Switch
-                checked={showCompanyName}
-                onCheckedChange={setShowCompanyName}
-              />
-            </div>
-            <div className="flex items-center justify-between rounded-lg border p-3">
-              <div>
-                <Label className="text-sm font-medium">{t('invoice.bankAccount')}</Label>
-                <p className="text-xs text-muted-foreground">
-                  {t('invoice.bankAccountHint')}
-                </p>
+
+              <SaveButton>
+                <Separator />
+                <div className="flex items-center gap-3">
+                  <Button onClick={handleSaveGeneral} disabled={saving}>
+                    {saving ? (
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    ) : (
+                      <Save className="mr-2 h-4 w-4" />
+                    )}
+                    {t('invoice.saveInvoice')}
+                  </Button>
+                </div>
+              </SaveButton>
+            </CardContent>
+          </Card>
+        </ReadOnlyWrapper>
+      ) : tab === "layout" ? (
+        <>
+          <ReadOnlyWrapper>
+            <div className="space-y-4">
+              <div className="flex items-center justify-between">
+                <div className="flex gap-1 rounded-lg border bg-muted p-1 max-w-xs">
+                  <button
+                    type="button"
+                    onClick={() => setLayoutDocType("invoice")}
+                    className={cn(
+                      "flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                      layoutDocType === "invoice"
+                        ? "bg-background text-foreground shadow-sm"
+                        : "text-muted-foreground hover:text-foreground"
+                    )}
+                  >
+                    {t('invoice.layoutDocInvoice')}
+                  </button>
+                  <button
+                    type="button"
+                    onClick={() => setLayoutDocType("quote")}
+                    className={cn(
+                      "flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                      layoutDocType === "quote"
+                        ? "bg-background text-foreground shadow-sm"
+                        : "text-muted-foreground hover:text-foreground"
+                    )}
+                  >
+                    {t('invoice.layoutDocQuote')}
+                  </button>
+                </div>
+                <div className="flex items-center gap-2 mr-2">
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={() => {
+                      if (layoutDocType === "invoice") {
+                        setInvoiceLayout(getDefaultInvoiceLayout());
+                      } else {
+                        setQuoteLayout(getDefaultInvoiceLayout());
+                      }
+                    }}
+                  >
+                    {t('invoice.resetLayout')}
+                  </Button>
+                  <Button onClick={handleSaveLayout} disabled={saving} size="sm">
+                    {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                    {t('invoice.saveLayout')}
+                  </Button>
+                </div>
               </div>
               </div>
-              <Switch
-                checked={showBankAccount}
-                onCheckedChange={setShowBankAccount}
-              />
-            </div>
-            <div className="flex items-center justify-between rounded-lg border p-3">
-              <div>
-                <Label className="text-sm font-medium">{t('invoice.organizationNumber')}</Label>
-                <p className="text-xs text-muted-foreground">
-                  {t('invoice.organizationNumberHint')}
-                </p>
+              <div className="grid gap-6 xl:grid-cols-2">
+                <InvoiceLayoutEditor
+                  config={layoutDocType === "invoice" ? invoiceLayout : quoteLayout}
+                  onChange={layoutDocType === "invoice" ? setInvoiceLayout : setQuoteLayout}
+                  documentType={layoutDocType}
+                  customFields={customFields}
+                />
+                <div className="hidden xl:block pr-2">
+                  <div className="sticky top-6 space-y-2">
+                    <p className="text-xs font-medium text-muted-foreground">{t('invoice.preview')}</p>
+                    <InvoiceLayoutPreview
+                      config={layoutDocType === "invoice" ? invoiceLayout : quoteLayout}
+                      documentType={layoutDocType}
+                      customFields={customFields}
+                      template={layoutDocType === "invoice" ? invoiceTemplate : quoteTemplate}
+                      logoUrl={settings[SETTING_KEYS.COMPANY_LOGO] || undefined}
+                    />
+                  </div>
+                </div>
               </div>
               </div>
-              <Switch
-                checked={showOrgNumber}
-                onCheckedChange={setShowOrgNumber}
-              />
             </div>
             </div>
-          </div>
-
-          <Separator />
-
-          <div className="space-y-2">
-            <Label htmlFor="footerNote">{t('invoice.customFooter')}</Label>
-            <Textarea
-              id="footerNote"
-              placeholder={t('invoice.footerPlaceholder')}
-              rows={2}
-              value={footerNote}
-              onChange={(e) => setFooterNote(e.target.value)}
-            />
-            <p className="text-xs text-muted-foreground">
-              {t('invoice.footerHint')}
-            </p>
-          </div>
-
+          </ReadOnlyWrapper>
           <SaveButton>
           <SaveButton>
-            <Separator />
-            <div className="flex items-center gap-3">
-              <Button onClick={handleSave} disabled={saving}>
-                {saving ? (
-                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-                ) : (
-                  <Save className="mr-2 h-4 w-4" />
-                )}
-                {t('invoice.saveInvoice')}
+            <div className="flex justify-end">
+              <Button onClick={handleSaveLayout} disabled={saving}>
+                {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                {t('invoice.saveLayout')}
               </Button>
               </Button>
             </div>
             </div>
           </SaveButton>
           </SaveButton>
-        </CardContent>
-      </Card>
-      </ReadOnlyWrapper>
+        </>
+      ) : (
+        <CustomFieldsManager
+          initialFields={customFields}
+          layoutConfig={layoutDocType === "invoice" ? invoiceLayout : quoteLayout}
+        />
+      )}
     </div>
     </div>
   );
   );
 }
 }

+ 29 - 2
src/app/(authenticated)/settings/invoice/page.tsx

@@ -1,9 +1,36 @@
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
+import { getLayoutData } from "@/lib/get-layout-data";
+import { getFeatures } from "@/lib/features";
+import { getInvoiceLayoutConfig, getQuoteLayoutConfig } from "@/features/settings/Actions/invoiceLayoutActions";
+import { getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
+import { redirect } from "next/navigation";
 import { InvoiceSettings } from "./invoice-settings";
 import { InvoiceSettings } from "./invoice-settings";
 
 
 export default async function InvoiceSettingsPage() {
 export default async function InvoiceSettingsPage() {
-  const result = await getSettings();
+  const data = await getLayoutData();
+
+  if (data.status === "unauthenticated") redirect("/auth/sign-in");
+  if (data.status === "no-organization") redirect("/onboarding");
+
+  const features = await getFeatures(data.organizationId);
+
+  const [result, invoiceLayoutResult, quoteLayoutResult, customFieldsResult] = await Promise.all([
+    getSettings(),
+    getInvoiceLayoutConfig(),
+    getQuoteLayoutConfig(),
+    features.customFields ? getFieldDefinitions() : Promise.resolve({ success: true, data: [] }),
+  ]);
+
   const settings = result.success && result.data ? result.data : {};
   const settings = result.success && result.data ? result.data : {};
+  const customFields = customFieldsResult.success && customFieldsResult.data ? customFieldsResult.data : [];
 
 
-  return <InvoiceSettings settings={settings} />;
+  return (
+    <InvoiceSettings
+      settings={settings}
+      initialInvoiceLayout={invoiceLayoutResult.success ? invoiceLayoutResult.data : undefined}
+      initialQuoteLayout={quoteLayoutResult.success ? quoteLayoutResult.data : undefined}
+      customFields={customFields}
+      customFieldsEnabled={features.customFields ?? false}
+    />
+  );
 }
 }

+ 1 - 2
src/app/(authenticated)/settings/settings-nav.tsx

@@ -24,7 +24,7 @@ import {
   Info,
   Info,
   Key,
   Key,
   Layout,
   Layout,
-  ListPlus,
+
   Mail,
   Mail,
   MessageSquare,
   MessageSquare,
   Palette,
   Palette,
@@ -64,7 +64,6 @@ const settingsCategories: SettingsCategory[] = [
       { key: "templates", href: "/settings/templates", icon: Layout, gate: "customTemplates" },
       { key: "templates", href: "/settings/templates", icon: Layout, gate: "customTemplates" },
       { key: "payment", href: "/settings/payment", icon: Banknote, gate: "payments" },
       { key: "payment", href: "/settings/payment", icon: Banknote, gate: "payments" },
       { key: "currency", href: "/settings/currency", icon: Coins },
       { key: "currency", href: "/settings/currency", icon: Coins },
-      { key: "customFields", href: "/settings/custom-fields", icon: ListPlus, gate: "customFields" },
     ],
     ],
   },
   },
   {
   {

+ 10 - 2
src/app/(authenticated)/settings/templates/page.tsx

@@ -7,7 +7,7 @@ import { FeatureLockedMessage } from "../feature-locked-message";
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
 import { getTemplates } from "@/features/inspections/Actions/templateActions";
 import { getTemplates } from "@/features/inspections/Actions/templateActions";
 import { getTranslations } from "next-intl/server";
 import { getTranslations } from "next-intl/server";
-
+import { getInvoiceLayoutConfig } from "@/features/settings/Actions/invoiceLayoutActions";
 export default async function TemplatePage() {
 export default async function TemplatePage() {
   const data = await getLayoutData();
   const data = await getLayoutData();
 
 
@@ -26,14 +26,17 @@ export default async function TemplatePage() {
     );
     );
   }
   }
 
 
-  const [result, inspectionTemplatesResult] = await Promise.all([
+  const [result, inspectionTemplatesResult, invoiceLayoutResult] = await Promise.all([
     getSettings([
     getSettings([
       SETTING_KEYS.INVOICE_PRIMARY_COLOR,
       SETTING_KEYS.INVOICE_PRIMARY_COLOR,
       SETTING_KEYS.INVOICE_FONT_FAMILY,
       SETTING_KEYS.INVOICE_FONT_FAMILY,
       SETTING_KEYS.INVOICE_HEADER_STYLE,
       SETTING_KEYS.INVOICE_HEADER_STYLE,
+      SETTING_KEYS.INVOICE_LOGO_SIZE,
       SETTING_KEYS.QUOTE_PRIMARY_COLOR,
       SETTING_KEYS.QUOTE_PRIMARY_COLOR,
       SETTING_KEYS.QUOTE_FONT_FAMILY,
       SETTING_KEYS.QUOTE_FONT_FAMILY,
       SETTING_KEYS.QUOTE_HEADER_STYLE,
       SETTING_KEYS.QUOTE_HEADER_STYLE,
+      SETTING_KEYS.QUOTE_LOGO_SIZE,
+      SETTING_KEYS.COMPANY_LOGO,
       SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY,
       SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY,
       SETTING_KEYS.SMS_TEMPLATE_QUOTE_READY,
       SETTING_KEYS.SMS_TEMPLATE_QUOTE_READY,
       SETTING_KEYS.SMS_TEMPLATE_INSPECTION_READY,
       SETTING_KEYS.SMS_TEMPLATE_INSPECTION_READY,
@@ -44,6 +47,7 @@ export default async function TemplatePage() {
       SETTING_KEYS.SMS_TEMPLATE_PAYMENT_RECEIVED,
       SETTING_KEYS.SMS_TEMPLATE_PAYMENT_RECEIVED,
     ]),
     ]),
     getTemplates(),
     getTemplates(),
+    getInvoiceLayoutConfig(),
   ]);
   ]);
 
 
   const settings = result.success && result.data ? result.data : {};
   const settings = result.success && result.data ? result.data : {};
@@ -75,15 +79,19 @@ export default async function TemplatePage() {
         primaryColor: settings[SETTING_KEYS.INVOICE_PRIMARY_COLOR] || "#d97706",
         primaryColor: settings[SETTING_KEYS.INVOICE_PRIMARY_COLOR] || "#d97706",
         fontFamily: settings[SETTING_KEYS.INVOICE_FONT_FAMILY] || "Helvetica",
         fontFamily: settings[SETTING_KEYS.INVOICE_FONT_FAMILY] || "Helvetica",
         headerStyle: settings[SETTING_KEYS.INVOICE_HEADER_STYLE] || "standard",
         headerStyle: settings[SETTING_KEYS.INVOICE_HEADER_STYLE] || "standard",
+        logoSize: Number(settings[SETTING_KEYS.INVOICE_LOGO_SIZE]) || 100,
       }}
       }}
       initialQuoteValues={{
       initialQuoteValues={{
         primaryColor: settings[SETTING_KEYS.QUOTE_PRIMARY_COLOR] || "#d97706",
         primaryColor: settings[SETTING_KEYS.QUOTE_PRIMARY_COLOR] || "#d97706",
         fontFamily: settings[SETTING_KEYS.QUOTE_FONT_FAMILY] || "Helvetica",
         fontFamily: settings[SETTING_KEYS.QUOTE_FONT_FAMILY] || "Helvetica",
         headerStyle: settings[SETTING_KEYS.QUOTE_HEADER_STYLE] || "standard",
         headerStyle: settings[SETTING_KEYS.QUOTE_HEADER_STYLE] || "standard",
+        logoSize: Number(settings[SETTING_KEYS.QUOTE_LOGO_SIZE]) || 100,
       }}
       }}
       inspectionTemplates={inspectionTemplates}
       inspectionTemplates={inspectionTemplates}
       smsEnabled={features.sms ?? false}
       smsEnabled={features.sms ?? false}
       initialSmsTemplates={smsTemplates}
       initialSmsTemplates={smsTemplates}
+      logoUrl={settings[SETTING_KEYS.COMPANY_LOGO] || undefined}
+      invoiceLayoutConfig={invoiceLayoutResult.success ? invoiceLayoutResult.data : undefined}
     />
     />
   );
   );
 }
 }

+ 92 - 231
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -1,6 +1,7 @@
 "use client";
 "use client";
 
 
-import { useState } from "react";
+import { useState, useCallback } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
@@ -22,11 +23,14 @@ import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard"
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
 import { TemplateListClient } from "@/features/inspections/Components/TemplateListClient";
 import { TemplateListClient } from "@/features/inspections/Components/TemplateListClient";
 import { Textarea } from "@/components/ui/textarea";
 import { Textarea } from "@/components/ui/textarea";
+import { InvoiceLayoutPreview } from "@/features/settings/Components/InvoiceLayoutPreview";
+import { type InvoiceLayoutConfig, getDefaultInvoiceLayout } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 interface TemplateValues {
 interface TemplateValues {
   primaryColor: string;
   primaryColor: string;
   fontFamily: string;
   fontFamily: string;
   headerStyle: string;
   headerStyle: string;
+  logoSize: number;
 }
 }
 
 
 type TabType = "invoice" | "quotation" | "inspections" | "sms";
 type TabType = "invoice" | "quotation" | "inspections" | "sms";
@@ -48,117 +52,20 @@ const colorPresets = [
   { key: "indigo", value: "#4f46e5" },
   { key: "indigo", value: "#4f46e5" },
 ];
 ];
 
 
-function PreviewHeader({
-  values,
-  headerStyle,
-  fontFamily,
-  documentLabel,
-}: {
-  values: TemplateValues;
-  headerStyle: string;
-  fontFamily: string;
-  documentLabel: string;
-}) {
-  const style = { fontFamily: fontMap[fontFamily] || "sans-serif" };
-  const docNumber = documentLabel === "QUOTE" ? "QT-1001" : "INV-1001";
-
-  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">{documentLabel}</p>
-            <p className="text-[9px] text-gray-500">{docNumber}</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">{documentLabel}</p>
-          <div className="flex gap-3 text-[9px] text-gray-500">
-            <span>{docNumber}</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 }}>
-            {documentLabel}
-          </p>
-          <p className="text-[9px] text-gray-500">{docNumber}</p>
-          <p className="text-[9px] text-gray-500">Jan 15, 2026</p>
-        </div>
-      </div>
-    </div>
-  );
-}
-
 function TemplateTab({
 function TemplateTab({
   values,
   values,
   setValues,
   setValues,
   documentLabel,
   documentLabel,
-  billToLabel,
+  documentType,
+  logoUrl,
+  invoiceLayoutConfig,
 }: {
 }: {
   values: TemplateValues;
   values: TemplateValues;
   setValues: (v: TemplateValues) => void;
   setValues: (v: TemplateValues) => void;
   documentLabel: string;
   documentLabel: string;
-  billToLabel: string;
+  documentType: "invoice" | "quote";
+  logoUrl?: string;
+  invoiceLayoutConfig?: InvoiceLayoutConfig;
 }) {
 }) {
   const t = useTranslations('settings');
   const t = useTranslations('settings');
   const currentPresetId = templatePresets.find(
   const currentPresetId = templatePresets.find(
@@ -172,6 +79,7 @@ function TemplateTab({
     const preset = templatePresets.find((p) => p.id === presetId);
     const preset = templatePresets.find((p) => p.id === presetId);
     if (preset) {
     if (preset) {
       setValues({
       setValues({
+        ...values,
         primaryColor: preset.primaryColor,
         primaryColor: preset.primaryColor,
         fontFamily: preset.fontFamily,
         fontFamily: preset.fontFamily,
         headerStyle: preset.headerStyle,
         headerStyle: preset.headerStyle,
@@ -312,35 +220,55 @@ function TemplateTab({
           <CardHeader>
           <CardHeader>
             <CardTitle className="text-base">{t('templates.fontAndLayout')}</CardTitle>
             <CardTitle className="text-base">{t('templates.fontAndLayout')}</CardTitle>
           </CardHeader>
           </CardHeader>
-          <CardContent className="space-y-4">
-            <div className="space-y-2">
-              <Label>{t('templates.fontFamily')}</Label>
-              <Select
-                value={values.fontFamily}
-                onValueChange={(v) => setValues({ ...values, fontFamily: v })}
-              >
-                <SelectTrigger><SelectValue /></SelectTrigger>
-                <SelectContent>
-                  <SelectItem value="Helvetica">{t('templates.helveticaDefault')}</SelectItem>
-                  <SelectItem value="Times-Roman">{t('templates.timesRoman')}</SelectItem>
-                  <SelectItem value="Courier">{t('templates.courier')}</SelectItem>
-                </SelectContent>
-              </Select>
-            </div>
+          <CardContent>
+            <div className="grid gap-4 sm:grid-cols-2">
+              <div className="space-y-4">
+                <div className="space-y-2">
+                  <Label>{t('templates.fontFamily')}</Label>
+                  <Select
+                    value={values.fontFamily}
+                    onValueChange={(v) => setValues({ ...values, fontFamily: v })}
+                  >
+                    <SelectTrigger><SelectValue /></SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="Helvetica">{t('templates.helveticaDefault')}</SelectItem>
+                      <SelectItem value="Times-Roman">{t('templates.timesRoman')}</SelectItem>
+                      <SelectItem value="Courier">{t('templates.courier')}</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
 
 
-            <div className="space-y-2">
-              <Label>{t('templates.headerStyle')}</Label>
-              <Select
-                value={values.headerStyle}
-                onValueChange={(v) => setValues({ ...values, headerStyle: v })}
-              >
-                <SelectTrigger><SelectValue /></SelectTrigger>
-                <SelectContent>
-                  <SelectItem value="standard">{t('templates.standard')}</SelectItem>
-                  <SelectItem value="compact">{t('templates.compact')}</SelectItem>
-                  <SelectItem value="modern">{t('templates.modern')}</SelectItem>
-                </SelectContent>
-              </Select>
+                <div className="space-y-2">
+                  <Label>{t('templates.headerStyle')}</Label>
+                  <Select
+                    value={values.headerStyle}
+                    onValueChange={(v) => setValues({ ...values, headerStyle: v })}
+                  >
+                    <SelectTrigger><SelectValue /></SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="standard">{t('templates.standard')}</SelectItem>
+                      <SelectItem value="compact">{t('templates.compact')}</SelectItem>
+                      <SelectItem value="modern">{t('templates.modern')}</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <Label>{t('templates.logoSize')}</Label>
+                <div className="flex items-center gap-3">
+                  <input
+                    type="range"
+                    min={50}
+                    max={200}
+                    step={10}
+                    value={values.logoSize}
+                    onChange={(e) => setValues({ ...values, logoSize: Number(e.target.value) })}
+                    className="flex-1"
+                  />
+                  <span className="w-12 text-right text-sm text-muted-foreground">{values.logoSize}%</span>
+                </div>
+              </div>
             </div>
             </div>
           </CardContent>
           </CardContent>
         </Card>
         </Card>
@@ -352,102 +280,12 @@ function TemplateTab({
           <CardTitle className="text-base">{t('templates.preview', { name: documentLabel })}</CardTitle>
           <CardTitle className="text-base">{t('templates.preview', { name: documentLabel })}</CardTitle>
         </CardHeader>
         </CardHeader>
         <CardContent>
         <CardContent>
-          <div
-            className="mx-auto max-w-[600px] rounded-lg border bg-white p-8 text-black"
-            style={{ fontFamily: fontMap[values.fontFamily] || "sans-serif" }}
-          >
-            <PreviewHeader
-              values={values}
-              headerStyle={values.headerStyle}
-              fontFamily={values.fontFamily}
-              documentLabel={documentLabel === t('templates.tabs.quotation') ? "QUOTE" : "INVOICE"}
-            />
-
-            {/* Bill To / Vehicle */}
-            <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 }}>
-                  {billToLabel}
-                </p>
-                <p className="text-sm font-medium">John Smith</p>
-                <p className="text-xs text-gray-500">john@example.com</p>
-              </div>
-              <div>
-                <p className="mb-1 text-[10px] font-semibold uppercase tracking-wider" style={{ color: values.primaryColor }}>
-                  {t('templates.vehicle')}
-                </p>
-                <p className="text-sm font-medium">2022 Toyota Camry</p>
-                <p className="text-xs text-gray-500">VIN: 1HGBH41...XMN</p>
-              </div>
-            </div>
-
-            {/* Parts Table */}
-            <table className="mb-1 w-full text-xs">
-              <thead>
-                <tr style={{ backgroundColor: `${values.primaryColor}15` }}>
-                  <th className="px-2 py-1.5 text-left font-medium">{t('templates.part')}</th>
-                  <th className="px-2 py-1.5 text-center font-medium">{t('templates.qty')}</th>
-                  <th className="px-2 py-1.5 text-right font-medium">{t('templates.price')}</th>
-                  <th className="px-2 py-1.5 text-right font-medium">{t('templates.total')}</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr className="border-b border-gray-100">
-                  <td className="px-2 py-1.5">Brake Pads (Front)</td>
-                  <td className="px-2 py-1.5 text-center">2</td>
-                  <td className="px-2 py-1.5 text-right">$45.00</td>
-                  <td className="px-2 py-1.5 text-right">$90.00</td>
-                </tr>
-                <tr className="border-b border-gray-100">
-                  <td className="px-2 py-1.5">Oil Filter</td>
-                  <td className="px-2 py-1.5 text-center">1</td>
-                  <td className="px-2 py-1.5 text-right">$12.00</td>
-                  <td className="px-2 py-1.5 text-right">$12.00</td>
-                </tr>
-              </tbody>
-            </table>
-
-            {/* Labor Table */}
-            <table className="mb-4 w-full text-xs">
-              <thead>
-                <tr style={{ backgroundColor: `${values.primaryColor}20` }}>
-                  <th className="px-2 py-1.5 text-left font-medium">{t('templates.labor')}</th>
-                  <th className="px-2 py-1.5 text-center font-medium">{t('templates.hours')}</th>
-                  <th className="px-2 py-1.5 text-right font-medium">{t('templates.rate')}</th>
-                  <th className="px-2 py-1.5 text-right font-medium">{t('templates.total')}</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr className="border-b border-gray-100">
-                  <td className="px-2 py-1.5">Brake Replacement</td>
-                  <td className="px-2 py-1.5 text-center">1.5</td>
-                  <td className="px-2 py-1.5 text-right">$85.00</td>
-                  <td className="px-2 py-1.5 text-right">$127.50</td>
-                </tr>
-              </tbody>
-            </table>
-
-            {/* Totals */}
-            <div className="flex justify-end">
-              <div className="w-48 space-y-1 text-xs">
-                <div className="flex justify-between">
-                  <span className="text-gray-500">{t('templates.subtotal')}</span>
-                  <span>$229.50</span>
-                </div>
-                <div className="flex justify-between">
-                  <span className="text-gray-500">{t('templates.tax', { rate: '8' })}</span>
-                  <span>$18.36</span>
-                </div>
-                <div
-                  className="flex justify-between border-t-2 pt-1 text-sm font-bold"
-                  style={{ borderColor: values.primaryColor, color: values.primaryColor }}
-                >
-                  <span>{t('templates.total')}</span>
-                  <span>$247.86</span>
-                </div>
-              </div>
-            </div>
-          </div>
+          <InvoiceLayoutPreview
+            config={invoiceLayoutConfig ?? getDefaultInvoiceLayout()}
+            documentType={documentType}
+            template={values}
+            logoUrl={logoUrl}
+          />
         </CardContent>
         </CardContent>
       </Card>
       </Card>
     </>
     </>
@@ -610,15 +448,32 @@ export function TemplateSettings({
   inspectionTemplates = [],
   inspectionTemplates = [],
   smsEnabled = false,
   smsEnabled = false,
   initialSmsTemplates = {},
   initialSmsTemplates = {},
+  logoUrl,
+  invoiceLayoutConfig,
 }: {
 }: {
   initialInvoiceValues: TemplateValues;
   initialInvoiceValues: TemplateValues;
   initialQuoteValues: TemplateValues;
   initialQuoteValues: TemplateValues;
   inspectionTemplates?: InspectionTemplate[];
   inspectionTemplates?: InspectionTemplate[];
   smsEnabled?: boolean;
   smsEnabled?: boolean;
   initialSmsTemplates?: Record<string, string>;
   initialSmsTemplates?: Record<string, string>;
+  logoUrl?: string;
+  invoiceLayoutConfig?: InvoiceLayoutConfig;
 }) {
 }) {
+  const router = useRouter();
+  const searchParams = useSearchParams();
   const t = useTranslations('settings');
   const t = useTranslations('settings');
-  const [tab, setTab] = useState<TabType>("invoice");
+
+  const tab = (searchParams.get("tab") as TabType) || "invoice";
+  const setTab = useCallback((newTab: TabType) => {
+    const params = new URLSearchParams(searchParams.toString());
+    if (newTab === "invoice") {
+      params.delete("tab");
+    } else {
+      params.set("tab", newTab);
+    }
+    const qs = params.toString();
+    router.replace(`/settings/templates${qs ? `?${qs}` : ""}`, { scroll: false });
+  }, [router, searchParams]);
   const [saving, setSaving] = useState(false);
   const [saving, setSaving] = useState(false);
   const [invoiceValues, setInvoiceValues] = useState(initialInvoiceValues);
   const [invoiceValues, setInvoiceValues] = useState(initialInvoiceValues);
   const [quoteValues, setQuoteValues] = useState(initialQuoteValues);
   const [quoteValues, setQuoteValues] = useState(initialQuoteValues);
@@ -632,6 +487,7 @@ export function TemplateSettings({
           setSetting(SETTING_KEYS.INVOICE_PRIMARY_COLOR, invoiceValues.primaryColor),
           setSetting(SETTING_KEYS.INVOICE_PRIMARY_COLOR, invoiceValues.primaryColor),
           setSetting(SETTING_KEYS.INVOICE_FONT_FAMILY, invoiceValues.fontFamily),
           setSetting(SETTING_KEYS.INVOICE_FONT_FAMILY, invoiceValues.fontFamily),
           setSetting(SETTING_KEYS.INVOICE_HEADER_STYLE, invoiceValues.headerStyle),
           setSetting(SETTING_KEYS.INVOICE_HEADER_STYLE, invoiceValues.headerStyle),
+          setSetting(SETTING_KEYS.INVOICE_LOGO_SIZE, String(invoiceValues.logoSize)),
         ]);
         ]);
         toast.success(t('templates.invoiceTemplateSaved'));
         toast.success(t('templates.invoiceTemplateSaved'));
       } else if (tab === "quotation") {
       } else if (tab === "quotation") {
@@ -639,6 +495,7 @@ export function TemplateSettings({
           setSetting(SETTING_KEYS.QUOTE_PRIMARY_COLOR, quoteValues.primaryColor),
           setSetting(SETTING_KEYS.QUOTE_PRIMARY_COLOR, quoteValues.primaryColor),
           setSetting(SETTING_KEYS.QUOTE_FONT_FAMILY, quoteValues.fontFamily),
           setSetting(SETTING_KEYS.QUOTE_FONT_FAMILY, quoteValues.fontFamily),
           setSetting(SETTING_KEYS.QUOTE_HEADER_STYLE, quoteValues.headerStyle),
           setSetting(SETTING_KEYS.QUOTE_HEADER_STYLE, quoteValues.headerStyle),
+          setSetting(SETTING_KEYS.QUOTE_LOGO_SIZE, String(quoteValues.logoSize)),
         ]);
         ]);
         toast.success(t('templates.quotationTemplateSaved'));
         toast.success(t('templates.quotationTemplateSaved'));
       } else if (tab === "sms") {
       } else if (tab === "sms") {
@@ -747,14 +604,18 @@ export function TemplateSettings({
                 values={invoiceValues}
                 values={invoiceValues}
                 setValues={setInvoiceValues}
                 setValues={setInvoiceValues}
                 documentLabel={t('templates.tabs.invoice')}
                 documentLabel={t('templates.tabs.invoice')}
-                billToLabel={t('templates.billTo')}
+                documentType="invoice"
+                logoUrl={logoUrl}
+                invoiceLayoutConfig={invoiceLayoutConfig}
               />
               />
             ) : (
             ) : (
               <TemplateTab
               <TemplateTab
                 values={quoteValues}
                 values={quoteValues}
                 setValues={setQuoteValues}
                 setValues={setQuoteValues}
                 documentLabel={t('templates.tabs.quotation')}
                 documentLabel={t('templates.tabs.quotation')}
-                billToLabel={t('templates.preparedFor')}
+                documentType="quote"
+                logoUrl={logoUrl}
+                invoiceLayoutConfig={invoiceLayoutConfig}
               />
               />
             )}
             )}
           </ReadOnlyWrapper>
           </ReadOnlyWrapper>

+ 557 - 331
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useState, useEffect, useCallback, useRef } from 'react'
+import React, { useState, useEffect, useCallback, useRef } from 'react'
 import {
 import {
   Camera,
   Camera,
   ChevronLeft,
   ChevronLeft,
@@ -16,6 +16,7 @@ import {
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
+import { isCustomFieldId, fromCustomFieldId, groupSectionsForRendering, getDefaultInvoiceLayout, getOrderedFieldIds, getVisibleFieldsForSection } from '@/features/settings/Schema/invoiceLayoutSchema'
 
 
 interface InvoiceRecord {
 interface InvoiceRecord {
   id: string
   id: string
@@ -93,6 +94,76 @@ interface InvoiceSettings {
   dueDays: number
   dueDays: number
 }
 }
 
 
+interface InvoiceLayoutConfig {
+  sections: Array<{
+    id: string
+    visible: boolean
+    order: number
+    column?: 'left' | 'right'
+    fields?: Array<{ id: string; visible: boolean }>
+  }>
+}
+
+interface CustomField {
+  label: string
+  value: string
+  fieldType: string
+  fieldId?: string
+}
+
+function isSectionVisible(config: InvoiceLayoutConfig | undefined, sectionId: string): boolean {
+  if (!config) return true
+  const section = config.sections.find(s => s.id === sectionId)
+  return section?.visible ?? true
+}
+
+function isFieldVisible(config: InvoiceLayoutConfig | undefined, sectionId: string, fieldId: string): boolean {
+  if (!config) return true
+  const section = config.sections.find(s => s.id === sectionId)
+  if (!section?.visible) return false
+  if (!section.fields) return true
+  const field = section.fields.find(f => f.id === fieldId)
+  return field?.visible ?? true
+}
+
+function getSectionOrder(config: InvoiceLayoutConfig | undefined): string[] {
+  if (!config) return ['header', 'customer', 'vehicle', 'service', 'parts_table', 'labor_table', 'totals', 'general', 'notes', 'diagnostic_notes', 'bank_account', 'footer']
+  return [...config.sections].sort((a, b) => a.order - b.order).map(s => s.id)
+}
+
+function getCustomFieldsForSection(
+  config: InvoiceLayoutConfig | null,
+  sectionId: string,
+  allCustomFields: CustomField[],
+): CustomField[] {
+  if (!config || !allCustomFields?.length) return []
+  const section = config.sections.find(s => s.id === sectionId)
+  if (!section?.fields) return []
+  const cfIds = new Set(
+    section.fields
+      .filter(f => f.visible !== false && isCustomFieldId(f.id))
+      .map(f => fromCustomFieldId(f.id))
+  )
+  return allCustomFields.filter(cf => cf.fieldId && cfIds.has(cf.fieldId))
+}
+
+function getUnassignedCustomFields(
+  config: InvoiceLayoutConfig | null,
+  allCustomFields: CustomField[],
+): CustomField[] {
+  if (!config || !allCustomFields?.length) return allCustomFields || []
+  const assignedFieldIds = new Set<string>()
+  for (const section of config.sections) {
+    if (!section.fields) continue
+    for (const f of section.fields) {
+      if (isCustomFieldId(f.id)) {
+        assignedFieldIds.add(fromCustomFieldId(f.id))
+      }
+    }
+  }
+  return allCustomFields.filter(cf => !cf.fieldId || !assignedFieldIds.has(cf.fieldId))
+}
+
 function hasContent(html: string | null): boolean {
 function hasContent(html: string | null): boolean {
   if (!html) return false
   if (!html) return false
   return html.replace(/<[^>]*>/g, '').trim().length > 0
   return html.replace(/<[^>]*>/g, '').trim().length > 0
@@ -115,7 +186,10 @@ export function InvoiceView({
   termsOfSaleUrl,
   termsOfSaleUrl,
   primaryColor = '#d97706',
   primaryColor = '#d97706',
   headerStyle = 'standard',
   headerStyle = 'standard',
+  logoSize = 100,
   portalUrl,
   portalUrl,
+  layoutConfig,
+  customFields = [],
 }: {
 }: {
   record: InvoiceRecord
   record: InvoiceRecord
   workshop: { name: string; address: string; phone: string; email: string }
   workshop: { name: string; address: string; phone: string; email: string }
@@ -133,7 +207,10 @@ export function InvoiceView({
   termsOfSaleUrl?: string
   termsOfSaleUrl?: string
   primaryColor?: string
   primaryColor?: string
   headerStyle?: string
   headerStyle?: string
+  logoSize?: number
   portalUrl?: string
   portalUrl?: string
+  layoutConfig?: InvoiceLayoutConfig
+  customFields?: CustomField[]
 }) {
 }) {
   const t = useTranslations('share.invoice')
   const t = useTranslations('share.invoice')
   const tc = useTranslations('share.common')
   const tc = useTranslations('share.common')
@@ -144,6 +221,7 @@ export function InvoiceView({
   const [paymentSuccess, setPaymentSuccess] = useState<{ amount: number } | null>(null)
   const [paymentSuccess, setPaymentSuccess] = useState<{ amount: number } | null>(null)
   const [verifying, setVerifying] = useState(false)
   const [verifying, setVerifying] = useState(false)
   const [customAmount, setCustomAmount] = useState(false)
   const [customAmount, setCustomAmount] = useState(false)
+  const [downloading, setDownloading] = useState(false)
 
 
   // Track view on mount
   // Track view on mount
   useEffect(() => {
   useEffect(() => {
@@ -165,6 +243,16 @@ export function InvoiceView({
   const balanceDue = displayTotal - totalPaid
   const balanceDue = displayTotal - totalPaid
   const shopName = workshop.name || record.shopName || 'Torqvoice'
   const shopName = workshop.name || record.shopName || 'Torqvoice'
 
 
+  // Layout config overrides for header field visibility
+  const effectiveShowLogo = layoutConfig
+    ? isFieldVisible(layoutConfig, 'header', 'logo')
+    : showLogo
+  const effectiveShowCompanyName = layoutConfig
+    ? isFieldVisible(layoutConfig, 'header', 'company_name')
+    : showCompanyName
+
+  const sectionOrder = getSectionOrder(layoutConfig)
+
   const showPaymentSection = enabledProviders.length > 0 && balanceDue > 0 && !paymentSuccess
   const showPaymentSection = enabledProviders.length > 0 && balanceDue > 0 && !paymentSuccess
 
 
   // Deduplicated image list for carousel
   // Deduplicated image list for carousel
@@ -270,15 +358,21 @@ export function InvoiceView({
   }, [verifyPayment])
   }, [verifyPayment])
 
 
   const handleDownloadPDF = async () => {
   const handleDownloadPDF = async () => {
-    const res = await fetch(`/api/public/share/invoice/${orgId}/${token}/pdf`)
-    if (!res.ok) return
-    const blob = await res.blob()
-    const url = URL.createObjectURL(blob)
-    const a = document.createElement('a')
-    a.href = url
-    a.download = `${invoiceNum}.pdf`
-    a.click()
-    URL.revokeObjectURL(url)
+    setDownloading(true)
+    try {
+      const res = await fetch(`/api/public/share/invoice/${orgId}/${token}/pdf`)
+      if (!res.ok) throw new Error('Failed')
+      const blob = await res.blob()
+      const url = URL.createObjectURL(blob)
+      const a = document.createElement('a')
+      a.href = url
+      a.download = `${invoiceNum}.pdf`
+      a.click()
+      URL.revokeObjectURL(url)
+    } catch {
+      // silent
+    }
+    setDownloading(false)
   }
   }
 
 
   const handlePayment = async (provider: string) => {
   const handlePayment = async (provider: string) => {
@@ -319,10 +413,11 @@ export function InvoiceView({
         <h1 className="text-2xl font-bold">{t('title')}</h1>
         <h1 className="text-2xl font-bold">{t('title')}</h1>
         <button
         <button
           onClick={handleDownloadPDF}
           onClick={handleDownloadPDF}
-          className="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white"
+          disabled={downloading}
+          className="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
           style={{ backgroundColor: primaryColor }}
           style={{ backgroundColor: primaryColor }}
         >
         >
-          <Download className="h-4 w-4" />
+          {downloading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
           {t('downloadPdf')}
           {t('downloadPdf')}
         </button>
         </button>
       </div>
       </div>
@@ -476,311 +571,469 @@ export function InvoiceView({
       )}
       )}
 
 
       <div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8 dark:bg-gray-900">
       <div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8 dark:bg-gray-900">
-        {/* Header */}
-        {headerStyle === 'modern' ? (
-          <>
-            <div className="rounded-lg p-6 text-center text-white" style={{ backgroundColor: primaryColor }}>
-              {showLogo && logoUrl && (
-                <img
-                  src={logoUrl}
-                  alt={shopName}
-                  className="mx-auto mb-2 max-h-16 max-w-[180px] object-contain"
-                />
-              )}
-              {showCompanyName && (
-                <h2 className="text-xl font-bold sm:text-2xl">{shopName}</h2>
-              )}
-              {workshop.address && <p className="mt-1 text-sm opacity-80">{workshop.address}</p>}
-              <div className="mt-1 flex flex-wrap justify-center gap-3 text-sm opacity-70">
-                {workshop.phone && <span>{t('tel', { phone: workshop.phone })}</span>}
-                {workshop.email && <span>{workshop.email}</span>}
-              </div>
-            </div>
-            <div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
-              <h3 className="text-xl font-bold uppercase">{t('title')}</h3>
-              <div className="flex gap-3 text-sm text-gray-500">
-                <span>{invoiceNum}</span>
-                <span>{serviceDate}</span>
-              </div>
-            </div>
-          </>
-        ) : headerStyle === 'compact' ? (
-          <div className="flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: '#e5e7eb' }}>
-            <div className="flex items-center gap-3">
-              {showLogo && logoUrl && (
-                <img
-                  src={logoUrl}
-                  alt={shopName}
-                  className="h-12 w-12 rounded object-contain"
-                />
-              )}
-              <div>
-                {showCompanyName && (
-                  <h2 className="text-lg font-bold" style={{ color: primaryColor }}>{shopName}</h2>
-                )}
-                {workshop.address && <p className="text-sm text-gray-500">{workshop.address}</p>}
-              </div>
-            </div>
-            <div className="sm:text-right">
-              <h3 className="text-lg font-bold uppercase">{t('title')}</h3>
-              <p className="text-sm text-gray-500">{invoiceNum}</p>
-              <p className="text-sm text-gray-500">{serviceDate}</p>
-            </div>
-          </div>
-        ) : (
-          /* Standard */
-          <div className="flex flex-col gap-4 border-b-2 pb-6 sm:flex-row sm:items-start sm:justify-between" style={{ borderColor: primaryColor }}>
-            <div>
-              {showLogo && logoUrl && (
-                <img
-                  src={logoUrl}
-                  alt={shopName}
-                  className="mb-2 max-h-16 max-w-[180px] object-contain object-left"
-                />
-              )}
-              {showCompanyName && (
-                <h2 className="text-xl font-bold sm:text-2xl" style={{ color: primaryColor }}>{shopName}</h2>
-              )}
-              {workshop.address && <p className="mt-1 text-sm text-gray-500">{workshop.address}</p>}
-              {workshop.phone && <p className="text-sm text-gray-500">{t('tel', { phone: workshop.phone })}</p>}
-              {workshop.email && <p className="text-sm text-gray-500">{workshop.email}</p>}
-            </div>
-            <div className="sm:text-right">
-              {showTorqvoiceBranding && (
-                <div className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-1 dark:bg-gray-800">
-                  <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
-                  <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
-                </div>
-              )}
-              <h3 className="text-xl font-bold uppercase" style={{ color: primaryColor }}>{t('title')}</h3>
-              <p className="mt-1 text-sm text-gray-500">{invoiceNum}</p>
-              <p className="text-sm text-gray-500">{serviceDate}</p>
-            </div>
-          </div>
-        )}
+        {(() => {
+          // Use column-based grouping for layout
+          const effectiveSections = layoutConfig?.sections ?? getDefaultInvoiceLayout().sections;
+          const groups = groupSectionsForRendering(effectiveSections);
+          // Build maps: which sections are in a column group, and which are paired
+          const columnGroupMap = new Map<string, { left: string[]; right: string[] }>();
+          const skipSet = new Set<string>();
+          for (const g of groups) {
+            if (g.type === 'columns') {
+              const allIds = [...g.left, ...g.right];
+              const firstId = allIds[0];
+              if (firstId) {
+                columnGroupMap.set(firstId, g);
+                for (const id of allIds.slice(1)) skipSet.add(id);
+              }
+            }
+          }
+          return sectionOrder.map((sectionId) => {
+          if (!isSectionVisible(layoutConfig, sectionId)) return null
+          if (skipSet.has(sectionId)) return null
 
 
-        {/* Info Boxes */}
-        <div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
-          {record.vehicle.customer && (
-            <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-              <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('billTo')}</p>
-              <p className="font-semibold">{record.vehicle.customer.name}</p>
-              {record.vehicle.customer.company && (
-                <p className="text-sm">{record.vehicle.customer.company}</p>
-              )}
-              {record.vehicle.customer.address && (
-                <p className="text-sm text-gray-500">{record.vehicle.customer.address}</p>
-              )}
-              {record.vehicle.customer.email && (
-                <p className="text-sm text-gray-500">{record.vehicle.customer.email}</p>
-              )}
-              {record.vehicle.customer.phone && (
-                <p className="text-sm text-gray-500">{record.vehicle.customer.phone}</p>
-              )}
-            </div>
-          )}
-          <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('vehicle')}</p>
-            <p className="font-semibold">{vehicleName}</p>
-            {record.vehicle.vin && (
-              <p className="text-sm text-gray-500">{t('vin', { vin: record.vehicle.vin })}</p>
-            )}
-            {record.vehicle.licensePlate && (
-              <p className="text-sm text-gray-500">{t('plate', { plate: record.vehicle.licensePlate })}</p>
-            )}
-          </div>
-          <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('service')}</p>
-            <p className="font-semibold">{record.title}</p>
-            <p className="text-sm text-gray-500">{t('type', { type: record.type })}</p>
-            {record.techName && <p className="text-sm text-gray-500">{t('tech', { tech: record.techName })}</p>}
-          </div>
-        </div>
+          switch (sectionId) {
+            case 'header':
+              return (
+                <div key="header">
+                  {headerStyle === 'modern' ? (
+                    <>
+                      <div className="rounded-lg p-6 text-center text-white" style={{ backgroundColor: primaryColor }}>
+                        {effectiveShowLogo && logoUrl && (
+                          <img
+                            src={logoUrl}
+                            alt={shopName}
+                            className="mx-auto mb-2 object-contain"
+                            style={{ maxHeight: 64 * (logoSize / 100), maxWidth: 180 * (logoSize / 100) }}
+                          />
+                        )}
+                        {effectiveShowCompanyName && (
+                          <h2 className="text-xl font-bold sm:text-2xl">{shopName}</h2>
+                        )}
+                        {workshop.address && <p className="mt-1 text-sm opacity-80">{workshop.address}</p>}
+                        <div className="mt-1 flex flex-wrap justify-center gap-3 text-sm opacity-70">
+                          {workshop.phone && <span>{t('tel', { phone: workshop.phone })}</span>}
+                          {workshop.email && <span>{workshop.email}</span>}
+                        </div>
+                      </div>
+                      <div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
+                        <h3 className="text-xl font-bold uppercase">{t('title')}</h3>
+                        <div className="flex gap-3 text-sm text-gray-500">
+                          <span>{invoiceNum}</span>
+                          <span>{serviceDate}</span>
+                        </div>
+                      </div>
+                    </>
+                  ) : headerStyle === 'compact' ? (
+                    <div className="flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: '#e5e7eb' }}>
+                      <div className="flex items-center gap-3">
+                        {effectiveShowLogo && logoUrl && (
+                          <img
+                            src={logoUrl}
+                            alt={shopName}
+                            className="rounded object-contain"
+                            style={{ height: 48 * (logoSize / 100), width: 48 * (logoSize / 100) }}
+                          />
+                        )}
+                        <div>
+                          {effectiveShowCompanyName && (
+                            <h2 className="text-lg font-bold" style={{ color: primaryColor }}>{shopName}</h2>
+                          )}
+                          {workshop.address && <p className="text-sm text-gray-500">{workshop.address}</p>}
+                        </div>
+                      </div>
+                      <div className="sm:text-right">
+                        <h3 className="text-lg font-bold uppercase">{t('title')}</h3>
+                        <p className="text-sm text-gray-500">{invoiceNum}</p>
+                        <p className="text-sm text-gray-500">{serviceDate}</p>
+                      </div>
+                    </div>
+                  ) : (
+                    /* Standard */
+                    <div className="flex flex-col gap-4 border-b-2 pb-6 sm:flex-row sm:items-start sm:justify-between" style={{ borderColor: primaryColor }}>
+                      <div>
+                        {effectiveShowLogo && logoUrl && (
+                          <img
+                            src={logoUrl}
+                            alt={shopName}
+                            className="mb-2 object-contain object-left"
+                            style={{ maxHeight: 64 * (logoSize / 100), maxWidth: 180 * (logoSize / 100) }}
+                          />
+                        )}
+                        {effectiveShowCompanyName && (
+                          <h2 className="text-xl font-bold sm:text-2xl" style={{ color: primaryColor }}>{shopName}</h2>
+                        )}
+                        {workshop.address && <p className="mt-1 text-sm text-gray-500">{workshop.address}</p>}
+                        {workshop.phone && <p className="text-sm text-gray-500">{t('tel', { phone: workshop.phone })}</p>}
+                        {workshop.email && <p className="text-sm text-gray-500">{workshop.email}</p>}
+                      </div>
+                      <div className="sm:text-right">
+                        {showTorqvoiceBranding && (
+                          <div className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-1 dark:bg-gray-800">
+                            <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
+                            <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+                          </div>
+                        )}
+                        <h3 className="text-xl font-bold uppercase" style={{ color: primaryColor }}>{t('title')}</h3>
+                        <p className="mt-1 text-sm text-gray-500">{invoiceNum}</p>
+                        <p className="text-sm text-gray-500">{serviceDate}</p>
+                      </div>
+                    </div>
+                  )}
+                </div>
+              )
 
 
-        {/* Parts */}
-        {record.partItems.length > 0 && (
-          <div className="mt-6">
-            <h4 className="mb-3 font-semibold">{t('parts')}</h4>
-            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
-              <table className="w-full min-w-125 text-sm">
-                <thead>
-                  <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
-                    <th className="p-2 font-medium">{t('partNumber')}</th>
-                    <th className="p-2 font-medium">{t('description')}</th>
-                    <th className="p-2 text-right font-medium">{t('qty')}</th>
-                    <th className="p-2 text-right font-medium">{t('unitPrice')}</th>
-                    <th className="p-2 text-right font-medium">{t('total')}</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {record.partItems.map((p, i) => (
-                    <tr key={i}>
-                      <td className="p-2 font-mono text-xs">{p.partNumber || '-'}</td>
-                      <td className="p-2">{p.name}</td>
-                      <td className="p-2 text-right">{p.quantity}</td>
-                      <td className="p-2 text-right">
-                        {formatCurrency(p.unitPrice, currencyCode)}
-                      </td>
-                      <td className="p-2 text-right font-medium">
-                        {formatCurrency(p.total, currencyCode)}
-                      </td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
-            </div>
-          </div>
-        )}
+            case 'customer':
+            case 'vehicle':
+            case 'service': {
+              const renderInfoCard = (sid: string) => {
+                if (sid === 'customer') {
+                  const c = record.vehicle.customer
+                  if (!c) return null
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'customer')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!(show('customer_name') || show('customer_email') || show('customer_phone') || show('customer_address') || show('customer_company'))) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['customer_name', 'customer_company', 'customer_address', 'customer_email', 'customer_phone'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'customer_name': return <p key={fid} className="font-semibold">{c.name}</p>
+                      case 'customer_company': return c.company ? <p key={fid} className="text-sm">{c.company}</p> : null
+                      case 'customer_address': return c.address ? <p key={fid} className="text-sm text-gray-500">{c.address}</p> : null
+                      case 'customer_email': return c.email ? <p key={fid} className="text-sm text-gray-500">{c.email}</p> : null
+                      case 'customer_phone': return c.phone ? <p key={fid} className="text-sm text-gray-500">{c.phone}</p> : null
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('billTo')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'customer', customFields).map((cf, i) => (
+                        <div key={`cf-cust-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                if (sid === 'vehicle') {
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'vehicle')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!(show('vehicle_name') || show('vin') || show('license_plate') || show('mileage'))) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['vehicle_name', 'vin', 'license_plate', 'mileage'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'vehicle_name': return <p key={fid} className="font-semibold">{vehicleName}</p>
+                      case 'vin': return record.vehicle.vin ? <p key={fid} className="text-sm text-gray-500">{t('vin', { vin: record.vehicle.vin })}</p> : null
+                      case 'license_plate': return record.vehicle.licensePlate ? <p key={fid} className="text-sm text-gray-500">{t('plate', { plate: record.vehicle.licensePlate })}</p> : null
+                      case 'mileage': return record.vehicle.mileage > 0 ? <p key={fid} className="text-sm text-gray-500">{record.vehicle.mileage}</p> : null
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('vehicle')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'vehicle', customFields).map((cf, i) => (
+                        <div key={`cf-veh-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                if (sid === 'service') {
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'service')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!(show('service_title') || show('service_type') || show('tech_name'))) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['service_title', 'service_type', 'tech_name'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'service_title': return <p key={fid} className="font-semibold">{record.title}</p>
+                      case 'service_type': return <p key={fid} className="text-sm text-gray-500">{t('type', { type: record.type })}</p>
+                      case 'tech_name': return record.techName ? <p key={fid} className="text-sm text-gray-500">{t('tech', { tech: record.techName })}</p> : null
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('service')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'service', customFields).map((cf, i) => (
+                        <div key={`cf-svc-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                return null;
+              };
 
 
-        {/* Labor */}
-        {record.laborItems.length > 0 && (
-          <div className="mt-6">
-            <h4 className="mb-3 font-semibold">{t('labor')}</h4>
-            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
-              <table className="w-full min-w-112.5 text-sm">
-                <thead>
-                  <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
-                    <th className="p-2 font-medium">{t('description')}</th>
-                    <th className="p-2 text-right font-medium">{t('hours')}</th>
-                    <th className="p-2 text-right font-medium">{t('rate')}</th>
-                    <th className="p-2 text-right font-medium">{t('total')}</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {record.laborItems.map((l, i) => (
-                    <tr key={i}>
-                      <td className="p-2">{l.description}</td>
-                      <td className="p-2 text-right">{l.hours}</td>
-                      <td className="p-2 text-right">{t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
-                      <td className="p-2 text-right font-medium">
-                        {formatCurrency(l.total, currencyCode)}
-                      </td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
-            </div>
-          </div>
-        )}
+              const colGroup = columnGroupMap.get(sectionId);
+              if (colGroup) {
+                return (
+                  <div key={`col-${sectionId}`} className="mt-6 grid gap-4 sm:grid-cols-2">
+                    <div className="space-y-4">{colGroup.left.map(id => <React.Fragment key={id}>{renderInfoCard(id)}</React.Fragment>)}</div>
+                    <div className="space-y-4">{colGroup.right.map(id => <React.Fragment key={id}>{renderInfoCard(id)}</React.Fragment>)}</div>
+                  </div>
+                );
+              }
+              return <div key={sectionId} className="mt-6">{renderInfoCard(sectionId)}</div>;
+            }
 
 
-        {/* Totals */}
-        <div className="mt-6 ml-auto max-w-xs space-y-2">
-          {record.subtotal > 0 && (
-            <div className="flex justify-between text-sm">
-              <span className="text-gray-500">{t('subtotal')}</span>
-              <span>{formatCurrency(record.subtotal, currencyCode)}</span>
-            </div>
-          )}
-          {record.discountAmount > 0 && (
-            <div className="flex justify-between text-sm">
-              <span className="text-gray-500">
-                {record.discountType === 'percentage' ? t('discountPercent', { percent: record.discountValue }) : t('discount')}
-              </span>
-              <span className="text-red-500">
-                {formatCurrency(-record.discountAmount, currencyCode)}
-              </span>
-            </div>
-          )}
-          {record.taxRate > 0 && (
-            <div className="flex justify-between text-sm">
-              <span className="text-gray-500">{t('tax', { rate: record.taxRate })}</span>
-              <span>{formatCurrency(record.taxAmount, currencyCode)}</span>
-            </div>
-          )}
-          <div
-            className={`border-t pt-2 ${totalPaid > 0 ? 'border-gray-200' : ''}`}
-            style={totalPaid > 0 ? undefined : { borderColor: primaryColor }}
-          >
-            <div
-              className={`flex justify-between ${totalPaid > 0 ? 'text-sm text-gray-500' : 'text-lg font-bold'}`}
-            >
-              <span>{t('total')}</span>
-              <span style={totalPaid > 0 ? undefined : { color: primaryColor }}>
-                {formatCurrency(displayTotal, currencyCode)}
-              </span>
-            </div>
-          </div>
-          {totalPaid > 0 && (
-            <>
-              <div className="flex justify-between text-sm text-emerald-600">
-                <span>{t('paid')}</span>
-                <span>{formatCurrency(-totalPaid, currencyCode)}</span>
-              </div>
-              {balanceDue <= 0 ? (
-                <div className="rounded-lg bg-emerald-50 px-4 py-3 dark:bg-emerald-900/20">
-                  <div className="flex justify-between text-lg font-bold text-emerald-600">
-                    <span>{t('balanceDue')}</span>
-                    <span>{t('paidInFull')}</span>
+            case 'parts_table':
+              if (record.partItems.length === 0) return null
+              return (
+                <div key="parts_table" className="mt-6">
+                  <h4 className="mb-3 font-semibold">{t('parts')}</h4>
+                  <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+                    <table className="w-full min-w-125 text-sm">
+                      <thead>
+                        <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
+                          <th className="p-2 font-medium">{t('partNumber')}</th>
+                          <th className="p-2 font-medium">{t('description')}</th>
+                          <th className="p-2 text-right font-medium">{t('qty')}</th>
+                          <th className="p-2 text-right font-medium">{t('unitPrice')}</th>
+                          <th className="p-2 text-right font-medium">{t('total')}</th>
+                        </tr>
+                      </thead>
+                      <tbody className="divide-y">
+                        {record.partItems.map((p, i) => (
+                          <tr key={i}>
+                            <td className="p-2 font-mono text-xs">{p.partNumber || '-'}</td>
+                            <td className="p-2">{p.name}</td>
+                            <td className="p-2 text-right">{p.quantity}</td>
+                            <td className="p-2 text-right">
+                              {formatCurrency(p.unitPrice, currencyCode)}
+                            </td>
+                            <td className="p-2 text-right font-medium">
+                              {formatCurrency(p.total, currencyCode)}
+                            </td>
+                          </tr>
+                        ))}
+                      </tbody>
+                    </table>
                   </div>
                   </div>
                 </div>
                 </div>
-              ) : (
-                <div className="rounded-lg bg-amber-50 px-4 py-3 ring-2 ring-amber-400 dark:bg-amber-900/20 dark:ring-amber-600">
-                  <div className="flex justify-between text-lg font-bold">
-                    <span>{t('amountDue')}</span>
-                    <span className="text-amber-700 dark:text-amber-400">
-                      {formatCurrency(balanceDue, currencyCode)}
-                    </span>
+              )
+
+            case 'labor_table':
+              if (record.laborItems.length === 0) return null
+              return (
+                <div key="labor_table" className="mt-6">
+                  <h4 className="mb-3 font-semibold">{t('labor')}</h4>
+                  <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+                    <table className="w-full min-w-112.5 text-sm">
+                      <thead>
+                        <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
+                          <th className="p-2 font-medium">{t('description')}</th>
+                          <th className="p-2 text-right font-medium">{t('hours')}</th>
+                          <th className="p-2 text-right font-medium">{t('rate')}</th>
+                          <th className="p-2 text-right font-medium">{t('total')}</th>
+                        </tr>
+                      </thead>
+                      <tbody className="divide-y">
+                        {record.laborItems.map((l, i) => (
+                          <tr key={i}>
+                            <td className="p-2">{l.description}</td>
+                            <td className="p-2 text-right">{l.hours}</td>
+                            <td className="p-2 text-right">{t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
+                            <td className="p-2 text-right font-medium">
+                              {formatCurrency(l.total, currencyCode)}
+                            </td>
+                          </tr>
+                        ))}
+                      </tbody>
+                    </table>
                   </div>
                   </div>
                 </div>
                 </div>
-              )}
-            </>
-          )}
-        </div>
-
-        {/* Torqvoice branding near totals */}
-        {showTorqvoiceBranding && (
-          <div className="mt-3 flex items-center justify-end gap-1.5">
-            <span className="text-xs text-gray-400">{tc('poweredBy')}</span>
-            <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-3.5 w-3.5" />
-            <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
-          </div>
-        )}
+              )
 
 
-        {/* Payment Information */}
-        {invoiceSettings &&
-          (invoiceSettings.bankAccount ||
-            invoiceSettings.orgNumber ||
-            invoiceSettings.paymentTerms) && (
-            <div className="mt-6 rounded-lg border p-4" style={{ borderColor: `${primaryColor}40`, backgroundColor: `${primaryColor}08` }}>
-              <p className="mb-2 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('paymentInformation')}</p>
-              <div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
-                {invoiceSettings.showBankAccount && invoiceSettings.bankAccount && (
-                  <div>
-                    <p className="text-xs text-gray-500 dark:text-gray-400">{t('bankAccount')}</p>
-                    <p className="font-medium">{invoiceSettings.bankAccount}</p>
-                  </div>
-                )}
-                {invoiceSettings.showOrgNumber && invoiceSettings.orgNumber && (
-                  <div>
-                    <p className="text-xs text-gray-500 dark:text-gray-400">{t('orgNumber')}</p>
-                    <p className="font-medium">{invoiceSettings.orgNumber}</p>
+            case 'totals':
+              return (
+                <div key="totals">
+                  <div className="mt-6 ml-auto max-w-xs space-y-2">
+                    {record.subtotal > 0 && (
+                      <div className="flex justify-between text-sm">
+                        <span className="text-gray-500">{t('subtotal')}</span>
+                        <span>{formatCurrency(record.subtotal, currencyCode)}</span>
+                      </div>
+                    )}
+                    {record.discountAmount > 0 && (
+                      <div className="flex justify-between text-sm">
+                        <span className="text-gray-500">
+                          {record.discountType === 'percentage' ? t('discountPercent', { percent: record.discountValue }) : t('discount')}
+                        </span>
+                        <span className="text-red-500">
+                          {formatCurrency(-record.discountAmount, currencyCode)}
+                        </span>
+                      </div>
+                    )}
+                    {record.taxRate > 0 && (
+                      <div className="flex justify-between text-sm">
+                        <span className="text-gray-500">{t('tax', { rate: record.taxRate })}</span>
+                        <span>{formatCurrency(record.taxAmount, currencyCode)}</span>
+                      </div>
+                    )}
+                    <div
+                      className={`border-t pt-2 ${totalPaid > 0 ? 'border-gray-200' : ''}`}
+                      style={totalPaid > 0 ? undefined : { borderColor: primaryColor }}
+                    >
+                      <div
+                        className={`flex justify-between ${totalPaid > 0 ? 'text-sm text-gray-500' : 'text-lg font-bold'}`}
+                      >
+                        <span>{t('total')}</span>
+                        <span style={totalPaid > 0 ? undefined : { color: primaryColor }}>
+                          {formatCurrency(displayTotal, currencyCode)}
+                        </span>
+                      </div>
+                    </div>
+                    {totalPaid > 0 && (
+                      <>
+                        <div className="flex justify-between text-sm text-emerald-600">
+                          <span>{t('paid')}</span>
+                          <span>{formatCurrency(-totalPaid, currencyCode)}</span>
+                        </div>
+                        {balanceDue <= 0 ? (
+                          <div className="rounded-lg bg-emerald-50 px-4 py-3 dark:bg-emerald-900/20">
+                            <div className="flex justify-between text-lg font-bold text-emerald-600">
+                              <span>{t('balanceDue')}</span>
+                              <span>{t('paidInFull')}</span>
+                            </div>
+                          </div>
+                        ) : (
+                          <div className="rounded-lg bg-amber-50 px-4 py-3 ring-2 ring-amber-400 dark:bg-amber-900/20 dark:ring-amber-600">
+                            <div className="flex justify-between text-lg font-bold">
+                              <span>{t('amountDue')}</span>
+                              <span className="text-amber-700 dark:text-amber-400">
+                                {formatCurrency(balanceDue, currencyCode)}
+                              </span>
+                            </div>
+                          </div>
+                        )}
+                      </>
+                    )}
                   </div>
                   </div>
-                )}
-                {invoiceSettings.paymentTerms && (
-                  <div>
-                    <p className="text-xs text-gray-500 dark:text-gray-400">{t('paymentTerms')}</p>
-                    <p className="font-medium">{invoiceSettings.paymentTerms}</p>
+
+                  {/* Torqvoice branding near totals */}
+                  {showTorqvoiceBranding && (
+                    <div className="mt-3 flex items-center justify-end gap-1.5">
+                      <span className="text-xs text-gray-400">{tc('poweredBy')}</span>
+                      <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-3.5 w-3.5" />
+                      <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+                    </div>
+                  )}
+                </div>
+              )
+
+            case 'general': {
+              const generalCfs = layoutConfig
+                ? getCustomFieldsForSection(layoutConfig, 'general', customFields)
+                : getUnassignedCustomFields(layoutConfig ?? null, customFields)
+              if (generalCfs.length === 0) return null
+              return (
+                <div key="general" className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-2 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('customFields', { defaultValue: 'Additional Information' })}</p>
+                  <div className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
+                    {generalCfs.map((cf, i) => (
+                      <div key={i}>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{cf.label}</p>
+                        <p className="font-medium">{cf.value}</p>
+                      </div>
+                    ))}
                   </div>
                   </div>
-                )}
-                {invoiceSettings.dueDays > 0 && (
-                  <div>
-                    <p className="text-xs text-gray-500 dark:text-gray-400">{t('dueDate')}</p>
-                    <p className="font-medium">
-                      {fmtDate(
-                        new Date(
-                          new Date(record.serviceDate).getTime() + invoiceSettings.dueDays * 86400000
-                        ),
-                        df,
-                        tz,
-                      )}
-                    </p>
+                </div>
+              )
+            }
+
+            case 'notes':
+              if (!hasContent(record.invoiceNotes)) return null
+              return (
+                <div key="notes" className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('notes')}</p>
+                  <div
+                    className="notes-content text-sm text-gray-600 dark:text-gray-400"
+                    dangerouslySetInnerHTML={{ __html: sanitizeHtml(record.invoiceNotes!) }}
+                  />
+                </div>
+              )
+
+            case 'diagnostic_notes':
+              if (!hasContent(record.diagnosticNotes)) return null
+              return (
+                <div key="diagnostic_notes" className="mt-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('diagnosticNotes')}</p>
+                  <div
+                    className="notes-content text-sm text-gray-600 dark:text-gray-400"
+                    dangerouslySetInnerHTML={{ __html: sanitizeHtml(record.diagnosticNotes!) }}
+                  />
+                </div>
+              )
+
+            case 'bank_account': {
+              const effectiveShowBankAccount = layoutConfig
+                ? isFieldVisible(layoutConfig, 'bank_account', 'bank_account')
+                : invoiceSettings?.showBankAccount ?? true
+              const effectiveShowOrgNumber = layoutConfig
+                ? isFieldVisible(layoutConfig, 'bank_account', 'org_number')
+                : invoiceSettings?.showOrgNumber ?? true
+              if (!invoiceSettings || (!invoiceSettings.bankAccount && !invoiceSettings.orgNumber && !invoiceSettings.paymentTerms)) return null
+              return (
+                <div key="bank_account" className="mt-6 rounded-lg border p-4" style={{ borderColor: `${primaryColor}40`, backgroundColor: `${primaryColor}08` }}>
+                  <p className="mb-2 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('paymentInformation')}</p>
+                  <div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
+                    {effectiveShowBankAccount && invoiceSettings.bankAccount && (
+                      <div>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{t('bankAccount')}</p>
+                        <p className="font-medium">{invoiceSettings.bankAccount}</p>
+                      </div>
+                    )}
+                    {effectiveShowOrgNumber && invoiceSettings.orgNumber && (
+                      <div>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{t('orgNumber')}</p>
+                        <p className="font-medium">{invoiceSettings.orgNumber}</p>
+                      </div>
+                    )}
+                    {invoiceSettings.paymentTerms && (
+                      <div>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{t('paymentTerms')}</p>
+                        <p className="font-medium">{invoiceSettings.paymentTerms}</p>
+                      </div>
+                    )}
+                    {invoiceSettings.dueDays > 0 && (
+                      <div>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{t('dueDate')}</p>
+                        <p className="font-medium">
+                          {fmtDate(
+                            new Date(
+                              new Date(record.serviceDate).getTime() + invoiceSettings.dueDays * 86400000
+                            ),
+                            df,
+                            tz,
+                          )}
+                        </p>
+                      </div>
+                    )}
                   </div>
                   </div>
-                )}
-              </div>
-            </div>
-          )}
+                </div>
+              )
+            }
+
+            case 'footer':
+              if (!invoiceSettings?.footerNote) return null
+              return (
+                <div key="footer" className="mt-4 border-t pt-4">
+                  <p className="whitespace-pre-wrap text-center text-xs text-gray-500 dark:text-gray-400">
+                    {invoiceSettings.footerNote}
+                  </p>
+                </div>
+              )
+
+            default:
+              return null
+          }
+        });
+        })()}
 
 
-        {/* Service Images */}
+        {/* Service Images (not part of layout config sections) */}
         {imageAttachments.length > 0 && (
         {imageAttachments.length > 0 && (
           <div className="mt-6">
           <div className="mt-6">
             <h4 className="mb-3 flex items-center gap-2 font-semibold">
             <h4 className="mb-3 flex items-center gap-2 font-semibold">
@@ -883,33 +1136,6 @@ export function InvoiceView({
               </div>
               </div>
             )
             )
           })()}
           })()}
-
-        {/* Notes */}
-        {hasContent(record.invoiceNotes) && (
-          <div className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('notes')}</p>
-            <div
-              className="notes-content text-sm text-gray-600 dark:text-gray-400"
-              dangerouslySetInnerHTML={{ __html: sanitizeHtml(record.invoiceNotes!) }}
-            />
-          </div>
-        )}
-        {hasContent(record.diagnosticNotes) && (
-          <div className="mt-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('diagnosticNotes')}</p>
-            <div
-              className="notes-content text-sm text-gray-600 dark:text-gray-400"
-              dangerouslySetInnerHTML={{ __html: sanitizeHtml(record.diagnosticNotes!) }}
-            />
-          </div>
-        )}
-        {invoiceSettings?.footerNote && (
-          <div className="mt-4 border-t pt-4">
-            <p className="whitespace-pre-wrap text-center text-xs text-gray-500 dark:text-gray-400">
-              {invoiceSettings.footerNote}
-            </p>
-          </div>
-        )}
       </div>
       </div>
 
 
       {portalUrl && (
       {portalUrl && (

+ 22 - 0
src/app/(public)/share/invoice/[orgId]/[token]/page.tsx

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
 import { InvoiceView } from "./invoice-view";
 import { InvoiceView } from "./invoice-view";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { resolvePortalOrg } from "@/lib/portal-slug";
 import { resolvePortalOrg } from "@/lib/portal-slug";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 import type { Metadata } from "next";
 import type { Metadata } from "next";
 
 
 /** Rewrites /api/protected/files/[orgId]/[category]/[filename] to /api/public/files/[token]/[category]/[filename] */
 /** Rewrites /api/protected/files/[orgId]/[category]/[filename] to /api/public/files/[token]/[category]/[filename] */
@@ -88,12 +89,14 @@ export default async function PublicInvoicePage({
             "invoice.showCompanyName",
             "invoice.showCompanyName",
             "invoice.primaryColor",
             "invoice.primaryColor",
             "invoice.headerStyle",
             "invoice.headerStyle",
+            "invoice.logoSize",
             "payment.providersEnabled",
             "payment.providersEnabled",
             "payment.termsOfSale",
             "payment.termsOfSale",
             "payment.termsOfSaleUrl",
             "payment.termsOfSaleUrl",
             "workshop.dateFormat",
             "workshop.dateFormat",
             "workshop.timezone",
             "workshop.timezone",
             "portal.enabled",
             "portal.enabled",
+            "invoice.layoutConfig",
           ],
           ],
         },
         },
       },
       },
@@ -107,6 +110,17 @@ export default async function PublicInvoicePage({
     getFeatures(orgId),
     getFeatures(orgId),
   ]);
   ]);
 
 
+  // Fetch custom field values for this service record
+  const customFieldValues = await db.customFieldValue.findMany({
+    where: { entityId: record.id, entityType: "service_record" },
+    include: { field: { select: { id: true, label: true, fieldType: true, isActive: true, sortOrder: true } } },
+    orderBy: { field: { sortOrder: "asc" } },
+  });
+
+  const customFields = customFieldValues
+    .filter(v => v.field.isActive && v.value)
+    .map(v => ({ label: v.field.label, value: v.value, fieldType: v.field.fieldType, fieldId: v.field.id }));
+
   const settingsMap: Record<string, string> = {};
   const settingsMap: Record<string, string> = {};
   for (const s of settings) settingsMap[s.key] = s.value;
   for (const s of settings) settingsMap[s.key] = s.value;
 
 
@@ -153,6 +167,11 @@ export default async function PublicInvoicePage({
       })),
       })),
   };
   };
 
 
+  // Parse layout config
+  const layoutConfig = mergeWithDefaults(
+    settingsMap["invoice.layoutConfig"] ? JSON.parse(settingsMap["invoice.layoutConfig"]) : {}
+  );
+
   const termsOfSaleUrl = settingsMap["payment.termsOfSaleUrl"]
   const termsOfSaleUrl = settingsMap["payment.termsOfSaleUrl"]
     || (settingsMap["payment.termsOfSale"] ? `/share/terms/${orgId}` : undefined);
     || (settingsMap["payment.termsOfSale"] ? `/share/terms/${orgId}` : undefined);
 
 
@@ -182,7 +201,10 @@ export default async function PublicInvoicePage({
       termsOfSaleUrl={termsOfSaleUrl}
       termsOfSaleUrl={termsOfSaleUrl}
       primaryColor={settingsMap["invoice.primaryColor"] || "#d97706"}
       primaryColor={settingsMap["invoice.primaryColor"] || "#d97706"}
       headerStyle={settingsMap["invoice.headerStyle"] || "standard"}
       headerStyle={settingsMap["invoice.headerStyle"] || "standard"}
+      logoSize={Number(settingsMap["invoice.logoSize"]) || 100}
       portalUrl={portalUrl}
       portalUrl={portalUrl}
+      layoutConfig={layoutConfig}
+      customFields={customFields}
     />
     />
   );
   );
 }
 }

+ 22 - 0
src/app/(public)/share/quote/[orgId]/[token]/page.tsx

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
 import { QuoteView } from "./quote-view";
 import { QuoteView } from "./quote-view";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { resolvePortalOrg } from "@/lib/portal-slug";
 import { resolvePortalOrg } from "@/lib/portal-slug";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 import type { Metadata } from "next";
 import type { Metadata } from "next";
 
 
 export const revalidate = 60;
 export const revalidate = 60;
@@ -68,9 +69,11 @@ export default async function PublicQuotePage({
             "workshop.timezone",
             "workshop.timezone",
             "quote.primaryColor",
             "quote.primaryColor",
             "quote.headerStyle",
             "quote.headerStyle",
+            "quote.logoSize",
             "invoice.primaryColor",
             "invoice.primaryColor",
             "invoice.headerStyle",
             "invoice.headerStyle",
             "portal.enabled",
             "portal.enabled",
+            "quote.layoutConfig",
           ],
           ],
         },
         },
       },
       },
@@ -82,6 +85,17 @@ export default async function PublicQuotePage({
     getFeatures(orgId),
     getFeatures(orgId),
   ]);
   ]);
 
 
+  // Fetch custom field values for this quote
+  const customFieldValues = await db.customFieldValue.findMany({
+    where: { entityId: quote.id, entityType: "quote" },
+    include: { field: { select: { id: true, label: true, fieldType: true, isActive: true, sortOrder: true } } },
+    orderBy: { field: { sortOrder: "asc" } },
+  });
+
+  const customFields = customFieldValues
+    .filter(v => v.field.isActive && v.value)
+    .map(v => ({ label: v.field.label, value: v.value, fieldType: v.field.fieldType, fieldId: v.field.id }));
+
   const settingsMap: Record<string, string> = {};
   const settingsMap: Record<string, string> = {};
   for (const s of settings) settingsMap[s.key] = s.value;
   for (const s of settings) settingsMap[s.key] = s.value;
 
 
@@ -123,6 +137,11 @@ export default async function PublicQuotePage({
       ),
       ),
     }));
     }));
 
 
+  // Parse layout config
+  const layoutConfig = mergeWithDefaults(
+    settingsMap["quote.layoutConfig"] ? JSON.parse(settingsMap["quote.layoutConfig"]) : {}
+  );
+
   const primaryColor = settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706";
   const primaryColor = settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706";
   const headerStyle = settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard";
   const headerStyle = settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard";
 
 
@@ -147,9 +166,12 @@ export default async function PublicQuotePage({
       timezone={settingsMap["workshop.timezone"] || undefined}
       timezone={settingsMap["workshop.timezone"] || undefined}
       primaryColor={primaryColor}
       primaryColor={primaryColor}
       headerStyle={headerStyle}
       headerStyle={headerStyle}
+      logoSize={Number(settingsMap["quote.logoSize"]) || 100}
       portalUrl={portalUrl}
       portalUrl={portalUrl}
       imageAttachments={imageAttachments}
       imageAttachments={imageAttachments}
       documentAttachments={documentAttachments}
       documentAttachments={documentAttachments}
+      layoutConfig={layoutConfig}
+      customFields={customFields}
     />
     />
   );
   );
 }
 }

+ 489 - 263
src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx

@@ -1,10 +1,11 @@
 "use client";
 "use client";
 
 
 import { Camera, Check, ChevronLeft, ChevronRight, Download, FileText, Loader2, MessageSquare, X } from "lucide-react";
 import { Camera, Check, ChevronLeft, ChevronRight, Download, FileText, Loader2, MessageSquare, X } from "lucide-react";
-import { useState, useEffect, useCallback, useRef } from "react";
+import React, { useState, useEffect, useCallback, useRef } from "react";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from "@/lib/format";
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from "@/lib/format";
 import { sanitizeHtml } from "@/lib/sanitize-html";
 import { sanitizeHtml } from "@/lib/sanitize-html";
+import { isCustomFieldId, fromCustomFieldId, groupSectionsForRendering, getDefaultInvoiceLayout, getOrderedFieldIds, getVisibleFieldsForSection } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 interface QuoteRecord {
 interface QuoteRecord {
   id: string;
   id: string;
@@ -53,6 +54,76 @@ interface QuoteRecord {
   } | null;
   } | null;
 }
 }
 
 
+interface InvoiceLayoutConfig {
+  sections: Array<{
+    id: string;
+    visible: boolean;
+    order: number;
+    column?: 'left' | 'right';
+    fields?: Array<{ id: string; visible: boolean }>;
+  }>;
+}
+
+interface CustomField {
+  label: string;
+  value: string;
+  fieldType: string;
+  fieldId?: string;
+}
+
+function isSectionVisible(config: InvoiceLayoutConfig | undefined, sectionId: string): boolean {
+  if (!config) return true;
+  const section = config.sections.find(s => s.id === sectionId);
+  return section?.visible ?? true;
+}
+
+function isFieldVisible(config: InvoiceLayoutConfig | undefined, sectionId: string, fieldId: string): boolean {
+  if (!config) return true;
+  const section = config.sections.find(s => s.id === sectionId);
+  if (!section?.visible) return false;
+  if (!section.fields) return true;
+  const field = section.fields.find(f => f.id === fieldId);
+  return field?.visible ?? true;
+}
+
+function getSectionOrder(config: InvoiceLayoutConfig | undefined): string[] {
+  if (!config) return ['header', 'customer', 'vehicle', 'service', 'parts_table', 'labor_table', 'totals', 'general', 'notes', 'diagnostic_notes', 'bank_account', 'footer'];
+  return [...config.sections].sort((a, b) => a.order - b.order).map(s => s.id);
+}
+
+function getCustomFieldsForSection(
+  config: InvoiceLayoutConfig | null,
+  sectionId: string,
+  allCustomFields: CustomField[],
+): CustomField[] {
+  if (!config || !allCustomFields?.length) return [];
+  const section = config.sections.find(s => s.id === sectionId);
+  if (!section?.fields) return [];
+  const cfIds = new Set(
+    section.fields
+      .filter(f => f.visible !== false && isCustomFieldId(f.id))
+      .map(f => fromCustomFieldId(f.id))
+  );
+  return allCustomFields.filter(cf => cf.fieldId && cfIds.has(cf.fieldId));
+}
+
+function getUnassignedCustomFields(
+  config: InvoiceLayoutConfig | null,
+  allCustomFields: CustomField[],
+): CustomField[] {
+  if (!config || !allCustomFields?.length) return allCustomFields || [];
+  const assignedFieldIds = new Set<string>();
+  for (const section of config.sections) {
+    if (!section.fields) continue;
+    for (const f of section.fields) {
+      if (isCustomFieldId(f.id)) {
+        assignedFieldIds.add(fromCustomFieldId(f.id));
+      }
+    }
+  }
+  return allCustomFields.filter(cf => !cf.fieldId || !assignedFieldIds.has(cf.fieldId));
+}
+
 interface QuoteAttachmentView {
 interface QuoteAttachmentView {
   id: string;
   id: string;
   fileName: string;
   fileName: string;
@@ -76,9 +147,12 @@ export function QuoteView({
   timezone,
   timezone,
   primaryColor = "#d97706",
   primaryColor = "#d97706",
   headerStyle = "standard",
   headerStyle = "standard",
+  logoSize = 100,
   portalUrl,
   portalUrl,
   imageAttachments = [],
   imageAttachments = [],
   documentAttachments = [],
   documentAttachments = [],
+  layoutConfig,
+  customFields = [],
 }: {
 }: {
   quote: QuoteRecord;
   quote: QuoteRecord;
   workshop: { name: string; address: string; phone: string; email: string };
   workshop: { name: string; address: string; phone: string; email: string };
@@ -91,9 +165,12 @@ export function QuoteView({
   timezone?: string;
   timezone?: string;
   primaryColor?: string;
   primaryColor?: string;
   headerStyle?: string;
   headerStyle?: string;
+  logoSize?: number;
   portalUrl?: string;
   portalUrl?: string;
   imageAttachments?: QuoteAttachmentView[];
   imageAttachments?: QuoteAttachmentView[];
   documentAttachments?: QuoteAttachmentView[];
   documentAttachments?: QuoteAttachmentView[];
+  layoutConfig?: InvoiceLayoutConfig;
+  customFields?: CustomField[];
 }) {
 }) {
   const t = useTranslations('share.quote');
   const t = useTranslations('share.quote');
   const tc = useTranslations('share.common');
   const tc = useTranslations('share.common');
@@ -131,6 +208,16 @@ export function QuoteView({
   const validUntilDate = quote.validUntil ? fmtDate(quote.validUntil, df, tz) : null;
   const validUntilDate = quote.validUntil ? fmtDate(quote.validUntil, df, tz) : null;
   const shopName = workshop.name || "Torqvoice";
   const shopName = workshop.name || "Torqvoice";
 
 
+  // Layout config overrides for header field visibility
+  const effectiveShowLogo = layoutConfig
+    ? isFieldVisible(layoutConfig, 'header', 'logo')
+    : true;
+  const effectiveShowCompanyName = layoutConfig
+    ? isFieldVisible(layoutConfig, 'header', 'company_name')
+    : true;
+
+  const sectionOrder = getSectionOrder(layoutConfig);
+
   // Image carousel
   // Image carousel
   const openCarousel = (index: number) => setCarouselIndex(index);
   const openCarousel = (index: number) => setCarouselIndex(index);
   const closeCarousel = () => setCarouselIndex(null);
   const closeCarousel = () => setCarouselIndex(null);
@@ -224,275 +311,414 @@ export function QuoteView({
       </div>
       </div>
 
 
       <div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8 dark:bg-gray-900">
       <div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8 dark:bg-gray-900">
-        {/* Header */}
-        {headerStyle === "modern" ? (
-          <>
-            <div className="rounded-lg p-6 text-center text-white" style={{ backgroundColor: primaryColor }}>
-              <img
-                src={logoUrl || "/torqvoice_app_logo.png"}
-                alt={shopName}
-                className="mx-auto mb-2 max-h-16 max-w-[180px] object-contain"
-              />
-              <h2 className="text-xl font-bold sm:text-2xl">{shopName}</h2>
-              {workshop.address && <p className="mt-1 text-sm opacity-80">{workshop.address}</p>}
-              <div className="mt-1 flex flex-wrap justify-center gap-3 text-sm opacity-70">
-                {workshop.phone && <span>{t('tel', { phone: workshop.phone })}</span>}
-                {workshop.email && <span>{workshop.email}</span>}
-              </div>
-            </div>
-            <div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
-              <div className="flex items-center gap-3">
-                <h3 className="text-xl font-bold">{t('title').toUpperCase()}</h3>
-                <span className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
-                  status === "accepted"
-                    ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
-                    : status === "rejected"
-                    ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
-                    : status === "changes_requested"
-                    ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
-                    : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
-                }`}>
-                  {statusLabels[status] || status}
-                </span>
-              </div>
-              <div className="flex gap-3 text-sm text-gray-500">
-                <span>{quoteNum}</span>
-                <span>{createdDate}</span>
-                {validUntilDate && <span>{t('validUntil', { date: validUntilDate })}</span>}
-              </div>
-            </div>
-          </>
-        ) : headerStyle === "compact" ? (
-          <div className="flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: "#e5e7eb" }}>
-            <div className="flex items-center gap-3">
-              <img
-                src={logoUrl || "/torqvoice_app_logo.png"}
-                alt={shopName}
-                className="h-12 w-12 rounded object-contain"
-              />
-              <div>
-                <h2 className="text-lg font-bold" style={{ color: primaryColor }}>{shopName}</h2>
-                {workshop.address && <p className="text-sm text-gray-500">{workshop.address}</p>}
-              </div>
-            </div>
-            <div className="sm:text-right">
-              <h3 className="text-lg font-bold">{t('title').toUpperCase()}</h3>
-              <p className="text-sm text-gray-500">{quoteNum}</p>
-              <p className="text-sm text-gray-500">{createdDate}</p>
-              {validUntilDate && (
-                <p className="text-sm text-gray-500">{t('validUntil', { date: validUntilDate })}</p>
-              )}
-              <span className={`mt-1 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
-                status === "accepted"
-                  ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
-                  : status === "rejected"
-                  ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
-                  : status === "changes_requested"
-                  ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
-                  : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
-              }`}>
-                {statusLabels[status] || status}
-              </span>
-            </div>
-          </div>
-        ) : (
-          /* Standard */
-          <div className="flex flex-col gap-4 border-b-2 pb-6 sm:flex-row sm:items-start sm:justify-between" style={{ borderColor: primaryColor }}>
-            <div>
-              <img
-                src={logoUrl || "/torqvoice_app_logo.png"}
-                alt={shopName}
-                className="mb-2 max-h-16 max-w-[180px] object-contain object-left"
-              />
-              <h2 className="text-xl font-bold sm:text-2xl" style={{ color: primaryColor }}>{shopName}</h2>
-              {workshop.address && <p className="mt-1 text-sm text-gray-500">{workshop.address}</p>}
-              {workshop.phone && <p className="text-sm text-gray-500">{t('tel', { phone: workshop.phone })}</p>}
-              {workshop.email && <p className="text-sm text-gray-500">{workshop.email}</p>}
-            </div>
-            <div className="sm:text-right">
-              {showTorqvoiceBranding && (
-                <div className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-1 dark:bg-gray-800">
-                  <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
-                  <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+        {(() => {
+          // Use column-based grouping for layout
+          const effectiveSections = layoutConfig?.sections ?? getDefaultInvoiceLayout().sections;
+          const groups = groupSectionsForRendering(effectiveSections);
+          const columnGroupMap = new Map<string, { left: string[]; right: string[] }>();
+          const skipSet = new Set<string>();
+          for (const g of groups) {
+            if (g.type === 'columns') {
+              const allIds = [...g.left, ...g.right];
+              const firstId = allIds[0];
+              if (firstId) {
+                columnGroupMap.set(firstId, g);
+                for (const id of allIds.slice(1)) skipSet.add(id);
+              }
+            }
+          }
+          return sectionOrder.map((sectionId) => {
+          if (!isSectionVisible(layoutConfig, sectionId)) return null;
+          if (skipSet.has(sectionId)) return null;
+
+          switch (sectionId) {
+            case 'header':
+              return (
+                <div key="header">
+                  {headerStyle === "modern" ? (
+                    <>
+                      <div className="rounded-lg p-6 text-center text-white" style={{ backgroundColor: primaryColor }}>
+                        {effectiveShowLogo && (logoUrl || "/torqvoice_app_logo.png") && (
+                          <img
+                            src={logoUrl || "/torqvoice_app_logo.png"}
+                            alt={shopName}
+                            className="mx-auto mb-2 object-contain"
+                            style={{ maxHeight: 64 * (logoSize / 100), maxWidth: 180 * (logoSize / 100) }}
+                          />
+                        )}
+                        {effectiveShowCompanyName && (
+                          <h2 className="text-xl font-bold sm:text-2xl">{shopName}</h2>
+                        )}
+                        {workshop.address && <p className="mt-1 text-sm opacity-80">{workshop.address}</p>}
+                        <div className="mt-1 flex flex-wrap justify-center gap-3 text-sm opacity-70">
+                          {workshop.phone && <span>{t('tel', { phone: workshop.phone })}</span>}
+                          {workshop.email && <span>{workshop.email}</span>}
+                        </div>
+                      </div>
+                      <div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
+                        <div className="flex items-center gap-3">
+                          <h3 className="text-xl font-bold">{t('title').toUpperCase()}</h3>
+                          <span className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
+                            status === "accepted"
+                              ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
+                              : status === "rejected"
+                              ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+                              : status === "changes_requested"
+                              ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
+                              : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
+                          }`}>
+                            {statusLabels[status] || status}
+                          </span>
+                        </div>
+                        <div className="flex gap-3 text-sm text-gray-500">
+                          <span>{quoteNum}</span>
+                          <span>{createdDate}</span>
+                          {validUntilDate && <span>{t('validUntil', { date: validUntilDate })}</span>}
+                        </div>
+                      </div>
+                    </>
+                  ) : headerStyle === "compact" ? (
+                    <div className="flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: "#e5e7eb" }}>
+                      <div className="flex items-center gap-3">
+                        {effectiveShowLogo && (
+                          <img
+                            src={logoUrl || "/torqvoice_app_logo.png"}
+                            alt={shopName}
+                            className="rounded object-contain"
+                            style={{ height: 48 * (logoSize / 100), width: 48 * (logoSize / 100) }}
+                          />
+                        )}
+                        <div>
+                          {effectiveShowCompanyName && (
+                            <h2 className="text-lg font-bold" style={{ color: primaryColor }}>{shopName}</h2>
+                          )}
+                          {workshop.address && <p className="text-sm text-gray-500">{workshop.address}</p>}
+                        </div>
+                      </div>
+                      <div className="sm:text-right">
+                        <h3 className="text-lg font-bold">{t('title').toUpperCase()}</h3>
+                        <p className="text-sm text-gray-500">{quoteNum}</p>
+                        <p className="text-sm text-gray-500">{createdDate}</p>
+                        {validUntilDate && (
+                          <p className="text-sm text-gray-500">{t('validUntil', { date: validUntilDate })}</p>
+                        )}
+                        <span className={`mt-1 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
+                          status === "accepted"
+                            ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
+                            : status === "rejected"
+                            ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+                            : status === "changes_requested"
+                            ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
+                            : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
+                        }`}>
+                          {statusLabels[status] || status}
+                        </span>
+                      </div>
+                    </div>
+                  ) : (
+                    /* Standard */
+                    <div className="flex flex-col gap-4 border-b-2 pb-6 sm:flex-row sm:items-start sm:justify-between" style={{ borderColor: primaryColor }}>
+                      <div>
+                        {effectiveShowLogo && (
+                          <img
+                            src={logoUrl || "/torqvoice_app_logo.png"}
+                            alt={shopName}
+                            className="mb-2 object-contain object-left"
+                            style={{ maxHeight: 64 * (logoSize / 100), maxWidth: 180 * (logoSize / 100) }}
+                          />
+                        )}
+                        {effectiveShowCompanyName && (
+                          <h2 className="text-xl font-bold sm:text-2xl" style={{ color: primaryColor }}>{shopName}</h2>
+                        )}
+                        {workshop.address && <p className="mt-1 text-sm text-gray-500">{workshop.address}</p>}
+                        {workshop.phone && <p className="text-sm text-gray-500">{t('tel', { phone: workshop.phone })}</p>}
+                        {workshop.email && <p className="text-sm text-gray-500">{workshop.email}</p>}
+                      </div>
+                      <div className="sm:text-right">
+                        {showTorqvoiceBranding && (
+                          <div className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-1 dark:bg-gray-800">
+                            <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
+                            <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+                          </div>
+                        )}
+                        <h3 className="text-xl font-bold" style={{ color: primaryColor }}>{t('title').toUpperCase()}</h3>
+                        <p className="mt-1 text-sm text-gray-500">{quoteNum}</p>
+                        <p className="text-sm text-gray-500">{createdDate}</p>
+                        {validUntilDate && (
+                          <p className="text-sm text-gray-500">{t('validUntil', { date: validUntilDate })}</p>
+                        )}
+                        <span className={`mt-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
+                          status === "accepted"
+                            ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
+                            : status === "rejected"
+                            ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+                            : status === "changes_requested"
+                            ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
+                            : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
+                        }`}>
+                          {statusLabels[status] || status}
+                        </span>
+                      </div>
+                    </div>
+                  )}
                 </div>
                 </div>
-              )}
-              <h3 className="text-xl font-bold" style={{ color: primaryColor }}>{t('title').toUpperCase()}</h3>
-              <p className="mt-1 text-sm text-gray-500">{quoteNum}</p>
-              <p className="text-sm text-gray-500">{createdDate}</p>
-              {validUntilDate && (
-                <p className="text-sm text-gray-500">{t('validUntil', { date: validUntilDate })}</p>
-              )}
-              <span className={`mt-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
-                status === "accepted"
-                  ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
-                  : status === "rejected"
-                  ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
-                  : status === "changes_requested"
-                  ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
-                  : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
-              }`}>
-                {statusLabels[status] || status}
-              </span>
-            </div>
-          </div>
-        )}
-
-        {/* Info Boxes */}
-        <div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
-          {quote.customer && (
-            <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-              <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('preparedFor')}</p>
-              <p className="font-semibold">{quote.customer.name}</p>
-              {quote.customer.company && <p className="text-sm">{quote.customer.company}</p>}
-              {quote.customer.address && <p className="text-sm text-gray-500">{quote.customer.address}</p>}
-              {quote.customer.email && <p className="text-sm text-gray-500">{quote.customer.email}</p>}
-              {quote.customer.phone && <p className="text-sm text-gray-500">{quote.customer.phone}</p>}
-            </div>
-          )}
-          {quote.vehicle && (
-            <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-              <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('vehicle')}</p>
-              <p className="font-semibold">{quote.vehicle.year} {quote.vehicle.make} {quote.vehicle.model}</p>
-              {quote.vehicle.vin && <p className="text-sm text-gray-500">{t('vin', { vin: quote.vehicle.vin })}</p>}
-              {quote.vehicle.licensePlate && <p className="text-sm text-gray-500">{t('plate', { plate: quote.vehicle.licensePlate })}</p>}
-            </div>
-          )}
-          <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('quoteDetails')}</p>
-            <p className="font-semibold">{quote.title}</p>
-          </div>
-        </div>
-
-        {/* Description */}
-        {quote.description && (
-          <div className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
-            <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('description')}</p>
-            <div
-              className="notes-content text-sm text-gray-600 dark:text-gray-400"
-              dangerouslySetInnerHTML={{ __html: sanitizeHtml(quote.description) }}
-            />
-          </div>
-        )}
-
-        {/* Parts */}
-        {quote.partItems.length > 0 && (
-          <div className="mt-6">
-            <h4 className="mb-3 font-semibold">{t('parts')}</h4>
-            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
-              <table className="w-full min-w-125 text-sm">
-                <thead>
-                  <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
-                    <th className="p-2 font-medium">{t('partNumber')}</th>
-                    <th className="p-2 font-medium">{t('partDescription')}</th>
-                    <th className="p-2 text-right font-medium">{t('qty')}</th>
-                    <th className="p-2 text-right font-medium">{t('unitPrice')}</th>
-                    <th className="p-2 text-right font-medium">{t('total')}</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {quote.partItems.map((p, i) => (
-                    <tr key={i} className={p.excluded ? "line-through text-gray-400" : ""}>
-                      <td className="p-2 font-mono text-xs">{p.partNumber || "-"}</td>
-                      <td className="p-2">{p.name}</td>
-                      <td className="p-2 text-right">{p.quantity}</td>
-                      <td className="p-2 text-right">{formatCurrency(p.unitPrice, currencyCode)}</td>
-                      <td className="p-2 text-right font-medium">{formatCurrency(p.total, currencyCode)}</td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
-            </div>
-          </div>
-        )}
-
-        {/* Labor */}
-        {quote.laborItems.length > 0 && (
-          <div className="mt-6">
-            <h4 className="mb-3 font-semibold">{t('labor')}</h4>
-            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
-              <table className="w-full min-w-112.5 text-sm">
-                <thead>
-                  <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
-                    <th className="p-2 font-medium">{t('laborDescription')}</th>
-                    <th className="p-2 text-right font-medium">{t('hours')}</th>
-                    <th className="p-2 text-right font-medium">{t('rate')}</th>
-                    <th className="p-2 text-right font-medium">{t('total')}</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {quote.laborItems.map((l, i) => (
-                    <tr key={i} className={l.excluded ? "line-through text-gray-400" : ""}>
-                      <td className="p-2">{l.description}</td>
-                      <td className="p-2 text-right">{l.hours}</td>
-                      <td className="p-2 text-right">{t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
-                      <td className="p-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
-            </div>
-          </div>
-        )}
-
-        {/* Totals */}
-        <div className="mt-6 ml-auto max-w-xs space-y-2">
-          {(() => {
-            const laborTotal = quote.laborItems.reduce((sum, l) => l.excluded ? sum : sum + l.total, 0);
-            const partsTotal = quote.partItems.reduce((sum, p) => p.excluded ? sum : sum + p.total, 0);
-            const sub = laborTotal + partsTotal;
-            const disc = quote.discountType === "percentage"
-              ? sub * (quote.discountValue / 100)
-              : quote.discountType === "fixed"
-              ? Math.min(quote.discountValue, sub)
-              : 0;
-            const tax = (sub - disc) * (quote.taxRate / 100);
-            const total = sub - disc + tax;
-            return (
-              <>
-                {quote.laborItems.length > 0 && (
-                  <div className="flex justify-between text-sm">
-                    <span className="text-gray-500">{t('labor')}</span>
-                    <span>{formatCurrency(laborTotal, currencyCode)}</span>
-                  </div>
-                )}
-                {quote.partItems.length > 0 && (
-                  <div className="flex justify-between text-sm">
-                    <span className="text-gray-500">{t('parts')}</span>
-                    <span>{formatCurrency(partsTotal, currencyCode)}</span>
-                  </div>
-                )}
-                {sub > 0 && (
-                  <div className="flex justify-between text-sm">
-                    <span className="text-gray-500">{t('subtotal')}</span>
-                    <span>{formatCurrency(sub, currencyCode)}</span>
+              );
+
+            case 'customer':
+            case 'vehicle':
+            case 'service': {
+              const renderInfoCard = (sid: string) => {
+                if (sid === 'customer') {
+                  const c = quote.customer
+                  if (!c) return null
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'customer')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!(show('customer_name') || show('customer_email') || show('customer_phone') || show('customer_address') || show('customer_company'))) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['customer_name', 'customer_company', 'customer_address', 'customer_email', 'customer_phone'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'customer_name': return <p key={fid} className="font-semibold">{c.name}</p>
+                      case 'customer_company': return c.company ? <p key={fid} className="text-sm">{c.company}</p> : null
+                      case 'customer_address': return c.address ? <p key={fid} className="text-sm text-gray-500">{c.address}</p> : null
+                      case 'customer_email': return c.email ? <p key={fid} className="text-sm text-gray-500">{c.email}</p> : null
+                      case 'customer_phone': return c.phone ? <p key={fid} className="text-sm text-gray-500">{c.phone}</p> : null
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('preparedFor')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'customer', customFields).map((cf, i) => (
+                        <div key={`cf-cust-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                if (sid === 'vehicle') {
+                  if (!quote.vehicle) return null
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'vehicle')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!(show('vehicle_name') || show('vin') || show('license_plate'))) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['vehicle_name', 'vin', 'license_plate'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'vehicle_name': return <p key={fid} className="font-semibold">{quote.vehicle!.year} {quote.vehicle!.make} {quote.vehicle!.model}</p>
+                      case 'vin': return quote.vehicle!.vin ? <p key={fid} className="text-sm text-gray-500">{t('vin', { vin: quote.vehicle!.vin })}</p> : null
+                      case 'license_plate': return quote.vehicle!.licensePlate ? <p key={fid} className="text-sm text-gray-500">{t('plate', { plate: quote.vehicle!.licensePlate })}</p> : null
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('vehicle')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'vehicle', customFields).map((cf, i) => (
+                        <div key={`cf-veh-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                if (sid === 'service') {
+                  const vf = getVisibleFieldsForSection(layoutConfig, 'service')
+                  const show = (fid: string) => !vf || vf.has(fid)
+                  if (!show('service_title')) return null
+                  const fieldOrder = getOrderedFieldIds(vf, ['service_title'])
+                  const renderField = (fid: string) => {
+                    if (!show(fid)) return null
+                    switch (fid) {
+                      case 'service_title': return <p key={fid} className="font-semibold">{quote.title}</p>
+                      default: return null
+                    }
+                  }
+                  return (
+                    <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                      <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('quoteDetails')}</p>
+                      {fieldOrder.map(renderField)}
+                      {getCustomFieldsForSection(layoutConfig ?? null, 'service', customFields).map((cf, i) => (
+                        <div key={`cf-svc-${i}`} className="mt-1 text-sm"><span className="font-medium">{cf.label}:</span>{' '}<span className="text-gray-500">{cf.value}</span></div>
+                      ))}
+                    </div>
+                  );
+                }
+                return null;
+              };
+
+              const colGroup = columnGroupMap.get(sectionId);
+              if (colGroup) {
+                return (
+                  <div key={`col-${sectionId}`} className="mt-6 grid gap-4 sm:grid-cols-2">
+                    <div className="space-y-4">{colGroup.left.map(id => <React.Fragment key={id}>{renderInfoCard(id)}</React.Fragment>)}</div>
+                    <div className="space-y-4">{colGroup.right.map(id => <React.Fragment key={id}>{renderInfoCard(id)}</React.Fragment>)}</div>
                   </div>
                   </div>
-                )}
-                {disc > 0 && (
-                  <div className="flex justify-between text-sm">
-                    <span className="text-gray-500">
-                      {quote.discountType === "percentage" ? t('discountPercent', { percent: quote.discountValue }) : t('discount')}
-                    </span>
-                    <span className="text-red-500">{formatCurrency(-disc, currencyCode)}</span>
+                );
+              }
+              return <div key={sectionId} className="mt-6">{renderInfoCard(sectionId)}</div>;
+            }
+
+            case 'notes':
+              if (!quote.description) return null;
+              return (
+                <div key="notes" className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-1 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('description')}</p>
+                  <div
+                    className="notes-content text-sm text-gray-600 dark:text-gray-400"
+                    dangerouslySetInnerHTML={{ __html: sanitizeHtml(quote.description) }}
+                  />
+                </div>
+              );
+
+            case 'parts_table':
+              if (quote.partItems.length === 0) return null;
+              return (
+                <div key="parts_table" className="mt-6">
+                  <h4 className="mb-3 font-semibold">{t('parts')}</h4>
+                  <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+                    <table className="w-full min-w-125 text-sm">
+                      <thead>
+                        <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
+                          <th className="p-2 font-medium">{t('partNumber')}</th>
+                          <th className="p-2 font-medium">{t('partDescription')}</th>
+                          <th className="p-2 text-right font-medium">{t('qty')}</th>
+                          <th className="p-2 text-right font-medium">{t('unitPrice')}</th>
+                          <th className="p-2 text-right font-medium">{t('total')}</th>
+                        </tr>
+                      </thead>
+                      <tbody className="divide-y">
+                        {quote.partItems.map((p, i) => (
+                          <tr key={i} className={p.excluded ? "line-through text-gray-400" : ""}>
+                            <td className="p-2 font-mono text-xs">{p.partNumber || "-"}</td>
+                            <td className="p-2">{p.name}</td>
+                            <td className="p-2 text-right">{p.quantity}</td>
+                            <td className="p-2 text-right">{formatCurrency(p.unitPrice, currencyCode)}</td>
+                            <td className="p-2 text-right font-medium">{formatCurrency(p.total, currencyCode)}</td>
+                          </tr>
+                        ))}
+                      </tbody>
+                    </table>
                   </div>
                   </div>
-                )}
-                {quote.taxRate > 0 && (
-                  <div className="flex justify-between text-sm">
-                    <span className="text-gray-500">{t('tax', { rate: quote.taxRate })}</span>
-                    <span>{formatCurrency(tax, currencyCode)}</span>
+                </div>
+              );
+
+            case 'labor_table':
+              if (quote.laborItems.length === 0) return null;
+              return (
+                <div key="labor_table" className="mt-6">
+                  <h4 className="mb-3 font-semibold">{t('labor')}</h4>
+                  <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+                    <table className="w-full min-w-112.5 text-sm">
+                      <thead>
+                        <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
+                          <th className="p-2 font-medium">{t('laborDescription')}</th>
+                          <th className="p-2 text-right font-medium">{t('hours')}</th>
+                          <th className="p-2 text-right font-medium">{t('rate')}</th>
+                          <th className="p-2 text-right font-medium">{t('total')}</th>
+                        </tr>
+                      </thead>
+                      <tbody className="divide-y">
+                        {quote.laborItems.map((l, i) => (
+                          <tr key={i} className={l.excluded ? "line-through text-gray-400" : ""}>
+                            <td className="p-2">{l.description}</td>
+                            <td className="p-2 text-right">{l.hours}</td>
+                            <td className="p-2 text-right">{t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
+                            <td className="p-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
+                          </tr>
+                        ))}
+                      </tbody>
+                    </table>
                   </div>
                   </div>
-                )}
-                <div className="border-t pt-2" style={{ borderColor: primaryColor }}>
-                  <div className="flex justify-between text-lg font-bold">
-                    <span>{t('total')}</span>
-                    <span style={{ color: primaryColor }}>{formatCurrency(total, currencyCode)}</span>
+                </div>
+              );
+
+            case 'totals':
+              return (
+                <div key="totals" className="mt-6 ml-auto max-w-xs space-y-2">
+                  {(() => {
+                    const laborTotal = quote.laborItems.reduce((sum, l) => l.excluded ? sum : sum + l.total, 0);
+                    const partsTotal = quote.partItems.reduce((sum, p) => p.excluded ? sum : sum + p.total, 0);
+                    const sub = laborTotal + partsTotal;
+                    const disc = quote.discountType === "percentage"
+                      ? sub * (quote.discountValue / 100)
+                      : quote.discountType === "fixed"
+                      ? Math.min(quote.discountValue, sub)
+                      : 0;
+                    const tax = (sub - disc) * (quote.taxRate / 100);
+                    const total = sub - disc + tax;
+                    return (
+                      <>
+                        {quote.laborItems.length > 0 && (
+                          <div className="flex justify-between text-sm">
+                            <span className="text-gray-500">{t('labor')}</span>
+                            <span>{formatCurrency(laborTotal, currencyCode)}</span>
+                          </div>
+                        )}
+                        {quote.partItems.length > 0 && (
+                          <div className="flex justify-between text-sm">
+                            <span className="text-gray-500">{t('parts')}</span>
+                            <span>{formatCurrency(partsTotal, currencyCode)}</span>
+                          </div>
+                        )}
+                        {sub > 0 && (
+                          <div className="flex justify-between text-sm">
+                            <span className="text-gray-500">{t('subtotal')}</span>
+                            <span>{formatCurrency(sub, currencyCode)}</span>
+                          </div>
+                        )}
+                        {disc > 0 && (
+                          <div className="flex justify-between text-sm">
+                            <span className="text-gray-500">
+                              {quote.discountType === "percentage" ? t('discountPercent', { percent: quote.discountValue }) : t('discount')}
+                            </span>
+                            <span className="text-red-500">{formatCurrency(-disc, currencyCode)}</span>
+                          </div>
+                        )}
+                        {quote.taxRate > 0 && (
+                          <div className="flex justify-between text-sm">
+                            <span className="text-gray-500">{t('tax', { rate: quote.taxRate })}</span>
+                            <span>{formatCurrency(tax, currencyCode)}</span>
+                          </div>
+                        )}
+                        <div className="border-t pt-2" style={{ borderColor: primaryColor }}>
+                          <div className="flex justify-between text-lg font-bold">
+                            <span>{t('total')}</span>
+                            <span style={{ color: primaryColor }}>{formatCurrency(total, currencyCode)}</span>
+                          </div>
+                        </div>
+                      </>
+                    );
+                  })()}
+                </div>
+              );
+
+            case 'general': {
+              const generalCfs = layoutConfig
+                ? getCustomFieldsForSection(layoutConfig, 'general', customFields)
+                : getUnassignedCustomFields(layoutConfig ?? null, customFields);
+              if (generalCfs.length === 0) return null;
+              return (
+                <div key="general" className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+                  <p className="mb-2 text-xs font-bold uppercase" style={{ color: primaryColor }}>{t('customFields', { defaultValue: 'Additional Information' })}</p>
+                  <div className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
+                    {generalCfs.map((cf, i) => (
+                      <div key={i}>
+                        <p className="text-xs text-gray-500 dark:text-gray-400">{cf.label}</p>
+                        <p className="font-medium">{cf.value}</p>
+                      </div>
+                    ))}
                   </div>
                   </div>
                 </div>
                 </div>
-              </>
-            );
-          })()}
-        </div>
+              );
+            }
+
+            default:
+              return null;
+          }
+        });
+        })()}
 
 
-        {/* Image Attachments */}
+        {/* Image Attachments (not part of layout config sections) */}
         {imageAttachments.length > 0 && (
         {imageAttachments.length > 0 && (
           <div className="mt-6">
           <div className="mt-6">
             <h4 className="mb-3 flex items-center gap-2 font-semibold">
             <h4 className="mb-3 flex items-center gap-2 font-semibold">

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

@@ -10,6 +10,7 @@ import { PDFDocument } from "pdf-lib";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -118,6 +119,17 @@ export async function GET(
       }
       }
     }
     }
 
 
+    // Fetch custom field values for the quote
+    const customFieldValues = await db.customFieldValue.findMany({
+      where: { entityId: quote.id, entityType: "quote" },
+      include: { field: { select: { label: true, fieldType: true, isActive: true, sortOrder: true } } },
+      orderBy: { field: { sortOrder: "asc" } },
+    });
+
+    const customFields = customFieldValues
+      .filter((v) => v.field.isActive && v.value)
+      .map((v) => ({ fieldId: v.fieldId, label: v.field.label, value: v.value, fieldType: v.field.fieldType }));
+
     // Check if Torqvoice branding should be shown
     // Check if Torqvoice branding should be shown
     const features = await getFeatures(ctx.organizationId);
     const features = await getFeatures(ctx.organizationId);
     let torqvoiceLogoDataUri: string | undefined;
     let torqvoiceLogoDataUri: string | undefined;
@@ -125,12 +137,21 @@ export async function GET(
       torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
       torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
     }
     }
 
 
+    // Fetch layout config for quotes
+    const layoutConfigSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: ctx.organizationId, key: "quote.layoutConfig" } },
+    });
+    const layoutConfig = mergeWithDefaults(
+      layoutConfigSetting?.value ? JSON.parse(layoutConfigSetting.value) : {}
+    );
+
     const template = {
     const template = {
       primaryColor: settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706",
       primaryColor: settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706",
       fontFamily: settingsMap["quote.fontFamily"] || settingsMap["invoice.fontFamily"] || "Helvetica",
       fontFamily: settingsMap["quote.fontFamily"] || settingsMap["invoice.fontFamily"] || "Helvetica",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       headerStyle: settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard",
       headerStyle: settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard",
+      logoSize: Number(settingsMap["quote.logoSize"]) || 100,
     };
     };
 
 
     const element = React.createElement(QuotePDF, {
     const element = React.createElement(QuotePDF, {
@@ -150,7 +171,9 @@ export async function GET(
       imageAttachments,
       imageAttachments,
       otherAttachments,
       otherAttachments,
       pdfAttachmentNames: pdfAttachments.map((a) => a.fileName),
       pdfAttachmentNames: pdfAttachments.map((a) => a.fileName),
+      customFields,
       labels,
       labels,
+      layoutConfig,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     }) as any;
     const quoteBuffer = await renderToBuffer(element);
     const quoteBuffer = await renderToBuffer(element);

+ 23 - 1
src/app/api/protected/services/[id]/pdf/route.ts

@@ -11,6 +11,7 @@ import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { formatDateForPdf } from "@/lib/format";
 import { formatDateForPdf } from "@/lib/format";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -81,6 +82,17 @@ export async function GET(
       return NextResponse.json({ error: "Record not found" }, { status: 404 });
       return NextResponse.json({ error: "Record not found" }, { status: 404 });
     }
     }
 
 
+    // Fetch custom field values for this service record
+    const customFieldValues = await db.customFieldValue.findMany({
+      where: { entityId: record.id, entityType: "service_record" },
+      include: { field: { select: { label: true, fieldType: true, isActive: true, sortOrder: true } } },
+      orderBy: { field: { sortOrder: "asc" } },
+    });
+
+    const customFields = customFieldValues
+      .filter(v => v.field.isActive && v.value)
+      .map(v => ({ fieldId: v.fieldId, label: v.field.label, value: v.value, fieldType: v.field.fieldType }));
+
     // Build settings map
     // Build settings map
     const settingsMap: Record<string, string> = {};
     const settingsMap: Record<string, string> = {};
     for (const s of settings) settingsMap[s.key] = s.value;
     for (const s of settings) settingsMap[s.key] = s.value;
@@ -177,12 +189,22 @@ export async function GET(
         }
         }
       : undefined;
       : undefined;
 
 
+    // Fetch layout config
+    const layoutConfigSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: ctx.organizationId, key: "invoice.layoutConfig" } },
+    });
+    const layoutConfig = mergeWithDefaults(
+      layoutConfigSetting?.value ? JSON.parse(layoutConfigSetting.value) : {}
+    );
+
     const template = {
     const template = {
       primaryColor: settingsMap["invoice.primaryColor"] || settingsMap["invoice.template.primaryColor"] || "#d97706",
       primaryColor: settingsMap["invoice.primaryColor"] || settingsMap["invoice.template.primaryColor"] || "#d97706",
       fontFamily: settingsMap["invoice.fontFamily"] || settingsMap["invoice.template.fontFamily"] || "Helvetica",
       fontFamily: settingsMap["invoice.fontFamily"] || settingsMap["invoice.template.fontFamily"] || "Helvetica",
       showLogo: (settingsMap["invoice.showLogo"] ?? settingsMap["invoice.template.showLogo"]) !== "false",
       showLogo: (settingsMap["invoice.showLogo"] ?? settingsMap["invoice.template.showLogo"]) !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       headerStyle: settingsMap["invoice.headerStyle"] || settingsMap["invoice.template.headerStyle"] || "standard",
       headerStyle: settingsMap["invoice.headerStyle"] || settingsMap["invoice.template.headerStyle"] || "standard",
+      logoSize: Number(settingsMap["invoice.logoSize"]) || 100,
+      layoutConfig,
     };
     };
 
 
     // Check if Torqvoice branding should be shown
     // Check if Torqvoice branding should be shown
@@ -201,7 +223,7 @@ export async function GET(
       : undefined;
       : undefined;
 
 
     const element = React.createElement(InvoicePDF, {
     const element = React.createElement(InvoicePDF, {
-      data: record,
+      data: { ...record, customFields },
       workshop: {
       workshop: {
         name: org?.name || "",
         name: org?.name || "",
         address: settingsMap["workshop.address"] || "",
         address: settingsMap["workshop.address"] || "",

+ 23 - 1
src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts

@@ -11,6 +11,7 @@ import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { rateLimit } from "@/lib/rate-limit";
 import { rateLimit } from "@/lib/rate-limit";
 import { formatDateForPdf } from "@/lib/format";
 import { formatDateForPdf } from "@/lib/format";
 import { resolvePortalOrg } from "@/lib/portal-slug";
 import { resolvePortalOrg } from "@/lib/portal-slug";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -73,6 +74,17 @@ export async function GET(
       return NextResponse.json({ error: "Not found" }, { status: 404 });
       return NextResponse.json({ error: "Not found" }, { status: 404 });
     }
     }
 
 
+    // Fetch custom field values for this service record
+    const customFieldValues = await db.customFieldValue.findMany({
+      where: { entityId: record.id, entityType: "service_record" },
+      include: { field: { select: { label: true, fieldType: true, isActive: true, sortOrder: true } } },
+      orderBy: { field: { sortOrder: "asc" } },
+    });
+
+    const customFields = customFieldValues
+      .filter(v => v.field.isActive && v.value)
+      .map(v => ({ fieldId: v.fieldId, label: v.field.label, value: v.value, fieldType: v.field.fieldType }));
+
     const [settings, org] = await Promise.all([
     const [settings, org] = await Promise.all([
       db.appSetting.findMany({
       db.appSetting.findMany({
         where: { organizationId: record.vehicle.organizationId },
         where: { organizationId: record.vehicle.organizationId },
@@ -136,12 +148,22 @@ export async function GET(
         }
         }
       : undefined;
       : undefined;
 
 
+    // Fetch layout config
+    const layoutConfigSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: record.vehicle.organizationId, key: "invoice.layoutConfig" } },
+    });
+    const layoutConfig = mergeWithDefaults(
+      layoutConfigSetting?.value ? JSON.parse(layoutConfigSetting.value) : {}
+    );
+
     const template = {
     const template = {
       primaryColor: settingsMap["invoice.primaryColor"] || "#d97706",
       primaryColor: settingsMap["invoice.primaryColor"] || "#d97706",
       fontFamily: settingsMap["invoice.fontFamily"] || "Helvetica",
       fontFamily: settingsMap["invoice.fontFamily"] || "Helvetica",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       headerStyle: settingsMap["invoice.headerStyle"] || "standard",
       headerStyle: settingsMap["invoice.headerStyle"] || "standard",
+      logoSize: Number(settingsMap["invoice.logoSize"]) || 100,
+      layoutConfig,
     };
     };
 
 
     // Check if Torqvoice branding should be shown
     // Check if Torqvoice branding should be shown
@@ -160,7 +182,7 @@ export async function GET(
       : undefined;
       : undefined;
 
 
     const element = React.createElement(InvoicePDF, {
     const element = React.createElement(InvoicePDF, {
-      data: record,
+      data: { ...record, customFields },
       workshop: {
       workshop: {
         name: org?.name || "",
         name: org?.name || "",
         address: settingsMap["workshop.address"] || "",
         address: settingsMap["workshop.address"] || "",

+ 23 - 0
src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts

@@ -10,6 +10,7 @@ import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { resolvePortalOrg } from "@/lib/portal-slug";
 import { resolvePortalOrg } from "@/lib/portal-slug";
+import { mergeWithDefaults } from "@/features/settings/Schema/invoiceLayoutSchema";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -128,18 +129,38 @@ export async function GET(
       }
       }
     }
     }
 
 
+    // Fetch custom field values for the quote
+    const customFieldValues = await db.customFieldValue.findMany({
+      where: { entityId: quote.id, entityType: "quote" },
+      include: { field: { select: { label: true, fieldType: true, isActive: true, sortOrder: true } } },
+      orderBy: { field: { sortOrder: "asc" } },
+    });
+
+    const customFields = customFieldValues
+      .filter((v) => v.field.isActive && v.value)
+      .map((v) => ({ fieldId: v.fieldId, label: v.field.label, value: v.value, fieldType: v.field.fieldType }));
+
     const features = await getFeatures(orgId);
     const features = await getFeatures(orgId);
     let torqvoiceLogoDataUri: string | undefined;
     let torqvoiceLogoDataUri: string | undefined;
     if (!features.brandingRemoved) {
     if (!features.brandingRemoved) {
       torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
       torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
     }
     }
 
 
+    // Fetch layout config for quotes
+    const layoutConfigSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: orgId, key: "quote.layoutConfig" } },
+    });
+    const layoutConfig = mergeWithDefaults(
+      layoutConfigSetting?.value ? JSON.parse(layoutConfigSetting.value) : {}
+    );
+
     const template = {
     const template = {
       primaryColor: settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706",
       primaryColor: settingsMap["quote.primaryColor"] || settingsMap["invoice.primaryColor"] || "#d97706",
       fontFamily: settingsMap["quote.fontFamily"] || settingsMap["invoice.fontFamily"] || "Helvetica",
       fontFamily: settingsMap["quote.fontFamily"] || settingsMap["invoice.fontFamily"] || "Helvetica",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showLogo: settingsMap["invoice.showLogo"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
       headerStyle: settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard",
       headerStyle: settingsMap["quote.headerStyle"] || settingsMap["invoice.headerStyle"] || "standard",
+      logoSize: Number(settingsMap["quote.logoSize"]) || 100,
     };
     };
 
 
     const appUrl = process.env.NEXT_PUBLIC_APP_URL
     const appUrl = process.env.NEXT_PUBLIC_APP_URL
@@ -168,7 +189,9 @@ export async function GET(
       imageAttachments,
       imageAttachments,
       otherAttachments,
       otherAttachments,
       pdfAttachmentNames: pdfAttachments.map((a) => a.fileName),
       pdfAttachmentNames: pdfAttachments.map((a) => a.fileName),
+      customFields,
       labels,
       labels,
+      layoutConfig,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     }) as any;
     const quoteBuffer = await renderToBuffer(element);
     const quoteBuffer = await renderToBuffer(element);

+ 1 - 1
src/app/layout.tsx

@@ -88,7 +88,7 @@ export default async function RootLayout({
         <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
         <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
         <script
         <script
           dangerouslySetInnerHTML={{
           dangerouslySetInnerHTML={{
-            __html: `(function(){try{var t=localStorage.getItem('torqvoice-theme')||'dark';if(t==='system'){t=matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.classList.add(t)}catch(e){}})()`,
+            __html: `(function(){try{if(location.pathname.indexOf('/share/')===0||location.pathname.indexOf('/share/')>0){document.documentElement.classList.add('light');return}var t=localStorage.getItem('torqvoice-theme')||'dark';if(t==='system'){t=matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.classList.add(t)}catch(e){}})()`,
           }}
           }}
         />
         />
       </head>
       </head>

+ 2 - 2
src/components/shared-link-card.tsx

@@ -93,7 +93,7 @@ export function SharedLinkCard({
 
 
       {/* Shared date */}
       {/* Shared date */}
       {sharedAt && (
       {sharedAt && (
-        <p className="text-xs text-muted-foreground">
+        <p className="text-xs text-muted-foreground" suppressHydrationWarning>
           {t("sidebar.sharedLink.sharedOn", {
           {t("sidebar.sharedLink.sharedOn", {
             date: new Date(sharedAt).toLocaleDateString(undefined, {
             date: new Date(sharedAt).toLocaleDateString(undefined, {
               month: "short",
               month: "short",
@@ -112,7 +112,7 @@ export function SharedLinkCard({
           }`}
           }`}
         />
         />
         {hasViews ? (
         {hasViews ? (
-          <span className="text-xs text-muted-foreground">
+          <span className="text-xs text-muted-foreground" suppressHydrationWarning>
             {t("sidebar.sharedLink.viewedTimes", { count: viewCount })}
             {t("sidebar.sharedLink.viewedTimes", { count: viewCount })}
             {lastViewedAt && (
             {lastViewedAt && (
               <>
               <>

+ 49 - 9
src/features/custom-fields/Components/CustomFieldsForm.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import { useEffect, useState } from "react";
+import { useEffect, useState, useCallback } from "react";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import { Textarea } from "@/components/ui/textarea";
@@ -15,6 +15,7 @@ import {
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { getCustomFieldValues, saveCustomFieldValues, getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
 import { getCustomFieldValues, saveCustomFieldValues, getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
 import { Loader2 } from "lucide-react";
 import { Loader2 } from "lucide-react";
+import { cn } from "@/lib/utils";
 
 
 interface FieldWithValue {
 interface FieldWithValue {
   id: string;
   id: string;
@@ -30,14 +31,17 @@ export function CustomFieldsForm({
   entityId,
   entityId,
   entityType,
   entityType,
   onValuesReady,
   onValuesReady,
+  onChange,
 }: {
 }: {
   entityId?: string;
   entityId?: string;
   entityType: "service_record" | "quote";
   entityType: "service_record" | "quote";
-  onValuesReady?: (save: () => Promise<void>) => void;
+  onValuesReady?: (save: () => Promise<{ valid: boolean }>) => void;
+  onChange?: () => void;
 }) {
 }) {
   const [fields, setFields] = useState<FieldWithValue[]>([]);
   const [fields, setFields] = useState<FieldWithValue[]>([]);
   const [values, setValues] = useState<Record<string, string>>({});
   const [values, setValues] = useState<Record<string, string>>({});
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
+  const [errors, setErrors] = useState<Record<string, string>>({});
 
 
   useEffect(() => {
   useEffect(() => {
     async function load() {
     async function load() {
@@ -61,18 +65,47 @@ export function CustomFieldsForm({
     load();
     load();
   }, [entityId, entityType]);
   }, [entityId, entityType]);
 
 
+  const validate = useCallback((): Record<string, string> => {
+    const newErrors: Record<string, string> = {};
+    for (const field of fields) {
+      if (field.required && field.fieldType !== "checkbox") {
+        const val = values[field.id] ?? field.value ?? "";
+        if (!val.trim()) {
+          newErrors[field.id] = `${field.label} is required`;
+        }
+      }
+    }
+    return newErrors;
+  }, [fields, values]);
+
   useEffect(() => {
   useEffect(() => {
     if (onValuesReady) {
     if (onValuesReady) {
       onValuesReady(async () => {
       onValuesReady(async () => {
+        const validationErrors = validate();
+        if (Object.keys(validationErrors).length > 0) {
+          setErrors(validationErrors);
+          return { valid: false };
+        }
+        setErrors({});
         if (entityId && Object.keys(values).length > 0) {
         if (entityId && Object.keys(values).length > 0) {
           await saveCustomFieldValues(entityId, entityType, values);
           await saveCustomFieldValues(entityId, entityType, values);
         }
         }
+        return { valid: true };
       });
       });
     }
     }
-  }, [onValuesReady, entityId, entityType, values]);
+  }, [onValuesReady, entityId, entityType, values, validate]);
 
 
   const updateValue = (fieldId: string, value: string) => {
   const updateValue = (fieldId: string, value: string) => {
     setValues((prev) => ({ ...prev, [fieldId]: value }));
     setValues((prev) => ({ ...prev, [fieldId]: value }));
+    // Clear error for field when user types
+    if (errors[fieldId]) {
+      setErrors((prev) => {
+        const next = { ...prev };
+        delete next[fieldId];
+        return next;
+      });
+    }
+    onChange?.();
   };
   };
 
 
   if (loading) {
   if (loading) {
@@ -95,10 +128,11 @@ export function CustomFieldsForm({
       <CardContent className="space-y-4">
       <CardContent className="space-y-4">
         {fields.map((field) => {
         {fields.map((field) => {
           const val = values[field.id] ?? field.value ?? "";
           const val = values[field.id] ?? field.value ?? "";
+          const hasError = !!errors[field.id];
           return (
           return (
             <div key={field.id} className="space-y-1.5">
             <div key={field.id} className="space-y-1.5">
               {field.fieldType !== "checkbox" && (
               {field.fieldType !== "checkbox" && (
-                <Label>
+                <Label className={cn(hasError && "text-destructive")}>
                   {field.label}
                   {field.label}
                   {field.required && <span className="text-destructive ml-1">*</span>}
                   {field.required && <span className="text-destructive ml-1">*</span>}
                 </Label>
                 </Label>
@@ -108,7 +142,7 @@ export function CustomFieldsForm({
                 <Input
                 <Input
                   value={val}
                   value={val}
                   onChange={(e) => updateValue(field.id, e.target.value)}
                   onChange={(e) => updateValue(field.id, e.target.value)}
-                  required={field.required}
+                  className={cn(hasError && "border-destructive focus-visible:ring-destructive")}
                 />
                 />
               )}
               )}
 
 
@@ -117,7 +151,7 @@ export function CustomFieldsForm({
                   type="number"
                   type="number"
                   value={val}
                   value={val}
                   onChange={(e) => updateValue(field.id, e.target.value)}
                   onChange={(e) => updateValue(field.id, e.target.value)}
-                  required={field.required}
+                  className={cn(hasError && "border-destructive focus-visible:ring-destructive")}
                 />
                 />
               )}
               )}
 
 
@@ -126,7 +160,7 @@ export function CustomFieldsForm({
                   type="date"
                   type="date"
                   value={val}
                   value={val}
                   onChange={(e) => updateValue(field.id, e.target.value)}
                   onChange={(e) => updateValue(field.id, e.target.value)}
-                  required={field.required}
+                  className={cn(hasError && "border-destructive focus-visible:ring-destructive")}
                 />
                 />
               )}
               )}
 
 
@@ -135,7 +169,7 @@ export function CustomFieldsForm({
                   value={val}
                   value={val}
                   onChange={(e) => updateValue(field.id, e.target.value)}
                   onChange={(e) => updateValue(field.id, e.target.value)}
                   rows={3}
                   rows={3}
-                  required={field.required}
+                  className={cn(hasError && "border-destructive focus-visible:ring-destructive")}
                 />
                 />
               )}
               )}
 
 
@@ -151,7 +185,9 @@ export function CustomFieldsForm({
 
 
               {field.fieldType === "select" && (
               {field.fieldType === "select" && (
                 <Select value={val || "none"} onValueChange={(v) => updateValue(field.id, v === "none" ? "" : v)}>
                 <Select value={val || "none"} onValueChange={(v) => updateValue(field.id, v === "none" ? "" : v)}>
-                  <SelectTrigger><SelectValue placeholder="Select..." /></SelectTrigger>
+                  <SelectTrigger className={cn(hasError && "border-destructive focus-visible:ring-destructive")}>
+                    <SelectValue placeholder="Select..." />
+                  </SelectTrigger>
                   <SelectContent>
                   <SelectContent>
                     {!field.required && <SelectItem value="none">None</SelectItem>}
                     {!field.required && <SelectItem value="none">None</SelectItem>}
                     {(field.options || "")
                     {(field.options || "")
@@ -166,6 +202,10 @@ export function CustomFieldsForm({
                   </SelectContent>
                   </SelectContent>
                 </Select>
                 </Select>
               )}
               )}
+
+              {hasError && (
+                <p className="text-xs text-destructive">{errors[field.id]}</p>
+              )}
             </div>
             </div>
           );
           );
         })}
         })}

+ 226 - 151
src/features/custom-fields/Components/CustomFieldsManager.tsx

@@ -1,75 +1,95 @@
-"use client";
+'use client'
 
 
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { useTranslations } from "next-intl";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Card, CardContent } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { Switch } from "@/components/ui/switch";
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslations } from 'next-intl'
+import { toast } from 'sonner'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Badge } from '@/components/ui/badge'
+import { Switch } from '@/components/ui/switch'
 import {
 import {
   Select,
   Select,
   SelectContent,
   SelectContent,
   SelectItem,
   SelectItem,
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
-} from "@/components/ui/select";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-} from "@/components/ui/dialog";
-import { useGlassModal } from "@/components/glass-modal";
-import { useConfirm } from "@/components/confirm-dialog";
+} from '@/components/ui/select'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { useGlassModal } from '@/components/glass-modal'
+import { useConfirm } from '@/components/confirm-dialog'
 import {
 import {
   createFieldDefinition,
   createFieldDefinition,
   updateFieldDefinition,
   updateFieldDefinition,
   deleteFieldDefinition,
   deleteFieldDefinition,
-} from "@/features/custom-fields/Actions/customFieldActions";
-import type { FieldType, EntityType } from "@/features/custom-fields/Schema/customFieldSchema";
-import { GripVertical, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
+} from '@/features/custom-fields/Actions/customFieldActions'
+import type { FieldType, EntityType } from '@/features/custom-fields/Schema/customFieldSchema'
+import {
+  type InvoiceLayoutConfig,
+  BUILTIN_SECTIONS,
+  CUSTOM_FIELD_PREFIX,
+} from '@/features/settings/Schema/invoiceLayoutSchema'
+import { Loader2, Pencil, Plus, Trash2 } from 'lucide-react'
 
 
 interface FieldDef {
 interface FieldDef {
-  id: string;
-  name: string;
-  label: string;
-  fieldType: string;
-  options: string | null;
-  required: boolean;
-  entityType: string;
-  sortOrder: number;
-  isActive: boolean;
+  id: string
+  name: string
+  label: string
+  fieldType: string
+  options: string | null
+  required: boolean
+  entityType: string
+  sortOrder: number
+  isActive: boolean
+}
+
+/**
+ * Find the layout section a custom field definition is assigned to.
+ * Returns the human-readable section name, or null if not assigned.
+ */
+function getSectionForField(
+  definitionId: string,
+  layoutConfig?: InvoiceLayoutConfig
+): string | null {
+  if (!layoutConfig) return null
+  const cfId = `${CUSTOM_FIELD_PREFIX}${definitionId}`
+  for (const section of layoutConfig.sections) {
+    if (section.fields?.some((f) => f.id === cfId)) {
+      const builtin = BUILTIN_SECTIONS.find((s) => s.id === section.id)
+      return builtin?.name ?? section.id
+    }
+  }
+  return null
 }
 }
 
 
 export function CustomFieldsManager({
 export function CustomFieldsManager({
   initialFields = [],
   initialFields = [],
+  layoutConfig,
 }: {
 }: {
-  initialFields?: FieldDef[];
+  initialFields?: FieldDef[]
+  layoutConfig?: InvoiceLayoutConfig
 }) {
 }) {
-  const router = useRouter();
-  const t = useTranslations('settings');
-  const modal = useGlassModal();
-  const confirm = useConfirm();
-  const [fields] = useState(initialFields);
-  const [showDialog, setShowDialog] = useState(false);
-  const [editing, setEditing] = useState<FieldDef | null>(null);
-  const [loading, setLoading] = useState(false);
-  const [filterEntity, setFilterEntity] = useState<string>("all");
+  const router = useRouter()
+  const t = useTranslations('settings')
+  const modal = useGlassModal()
+  const confirm = useConfirm()
+  const [fields] = useState(initialFields)
+  const [showDialog, setShowDialog] = useState(false)
+  const [editing, setEditing] = useState<FieldDef | null>(null)
+  const [loading, setLoading] = useState(false)
+  const [filterEntity, setFilterEntity] = useState<string>('all')
 
 
   const [formData, setFormData] = useState({
   const [formData, setFormData] = useState({
-    name: "",
-    label: "",
-    fieldType: "text" as FieldType,
-    options: "",
+    name: '',
+    label: '',
+    fieldType: 'text' as FieldType,
+    options: '',
     required: false,
     required: false,
-    entityType: "service_record" as EntityType,
+    entityType: 'service_record' as EntityType,
     sortOrder: 0,
     sortOrder: 0,
     isActive: true,
     isActive: true,
-  });
+  })
 
 
   const fieldTypeLabels: Record<string, string> = {
   const fieldTypeLabels: Record<string, string> = {
     text: t('customFields.fieldTypes.text'),
     text: t('customFields.fieldTypes.text'),
@@ -78,173 +98,207 @@ export function CustomFieldsManager({
     select: t('customFields.fieldTypes.select'),
     select: t('customFields.fieldTypes.select'),
     checkbox: t('customFields.fieldTypes.checkbox'),
     checkbox: t('customFields.fieldTypes.checkbox'),
     textarea: t('customFields.fieldTypes.textarea'),
     textarea: t('customFields.fieldTypes.textarea'),
-  };
+  }
 
 
   const entityTypeLabels: Record<string, string> = {
   const entityTypeLabels: Record<string, string> = {
     service_record: t('customFields.serviceRecord'),
     service_record: t('customFields.serviceRecord'),
     quote: t('customFields.quote'),
     quote: t('customFields.quote'),
-  };
+  }
 
 
   const resetForm = () => {
   const resetForm = () => {
     setFormData({
     setFormData({
-      name: "",
-      label: "",
-      fieldType: "text",
-      options: "",
+      name: '',
+      label: '',
+      fieldType: 'text',
+      options: '',
       required: false,
       required: false,
-      entityType: "service_record",
+      entityType: 'service_record',
       sortOrder: 0,
       sortOrder: 0,
       isActive: true,
       isActive: true,
-    });
-    setEditing(null);
-  };
+    })
+    setEditing(null)
+  }
 
 
   const openCreate = () => {
   const openCreate = () => {
-    resetForm();
-    setShowDialog(true);
-  };
+    resetForm()
+    setShowDialog(true)
+  }
 
 
   const openEdit = (field: FieldDef) => {
   const openEdit = (field: FieldDef) => {
-    setEditing(field);
+    setEditing(field)
     setFormData({
     setFormData({
       name: field.name,
       name: field.name,
       label: field.label,
       label: field.label,
       fieldType: field.fieldType as FieldType,
       fieldType: field.fieldType as FieldType,
-      options: field.options || "",
+      options: field.options || '',
       required: field.required,
       required: field.required,
       entityType: field.entityType as EntityType,
       entityType: field.entityType as EntityType,
       sortOrder: field.sortOrder,
       sortOrder: field.sortOrder,
       isActive: field.isActive,
       isActive: field.isActive,
-    });
-    setShowDialog(true);
-  };
+    })
+    setShowDialog(true)
+  }
 
 
   const handleSave = async () => {
   const handleSave = async () => {
-    setLoading(true);
+    setLoading(true)
     const payload = {
     const payload = {
       ...formData,
       ...formData,
-      options: formData.fieldType === "select" ? formData.options : undefined,
-    };
+      options: formData.fieldType === 'select' ? formData.options : undefined,
+    }
 
 
     const result = editing
     const result = editing
       ? await updateFieldDefinition({ id: editing.id, ...payload })
       ? await updateFieldDefinition({ id: editing.id, ...payload })
-      : await createFieldDefinition(payload);
+      : await createFieldDefinition(payload)
 
 
     if (result.success) {
     if (result.success) {
-      toast.success(editing ? t('customFields.fieldUpdated') : t('customFields.fieldCreated'));
-      setShowDialog(false);
-      resetForm();
-      router.refresh();
+      toast.success(editing ? t('customFields.fieldUpdated') : t('customFields.fieldCreated'))
+      setShowDialog(false)
+      resetForm()
+      router.refresh()
     } else {
     } else {
-      modal.open("error", "Error", result.error || t('customFields.failedSave'));
+      modal.open('error', 'Error', result.error || t('customFields.failedSave'))
     }
     }
-    setLoading(false);
-  };
+    setLoading(false)
+  }
 
 
   const handleDelete = async (field: FieldDef) => {
   const handleDelete = async (field: FieldDef) => {
     const ok = await confirm({
     const ok = await confirm({
       title: t('customFields.deleteTitle'),
       title: t('customFields.deleteTitle'),
       description: t('customFields.deleteDescription', { label: field.label }),
       description: t('customFields.deleteDescription', { label: field.label }),
-      confirmLabel: "Delete",
+      confirmLabel: 'Delete',
       destructive: true,
       destructive: true,
-    });
-    if (!ok) return;
-    const result = await deleteFieldDefinition(field.id);
+    })
+    if (!ok) return
+    const result = await deleteFieldDefinition(field.id)
     if (result.success) {
     if (result.success) {
-      toast.success(t('customFields.fieldDeleted'));
-      router.refresh();
+      toast.success(t('customFields.fieldDeleted'))
+      router.refresh()
     } else {
     } else {
-      modal.open("error", "Error", result.error || t('customFields.failedDelete'));
+      modal.open('error', 'Error', result.error || t('customFields.failedDelete'))
     }
     }
-  };
+  }
 
 
-  const filtered = filterEntity === "all" ? fields : fields.filter((f) => f.entityType === filterEntity);
+  const filtered =
+    filterEntity === 'all' ? fields : fields.filter((f) => f.entityType === filterEntity)
 
 
   return (
   return (
-    <div className="space-y-6">
+    <div className="space-y-4">
       <div className="flex items-center justify-between">
       <div className="flex items-center justify-between">
-        <div>
-          <h2 className="text-lg font-semibold">{t('customFields.title')}</h2>
-          <p className="text-sm text-muted-foreground">
-            {t('customFields.description')}
-          </p>
+        <div className="flex items-center gap-3">
+          {['all', 'service_record', 'quote'].map((key) => (
+            <button
+              key={key}
+              type="button"
+              onClick={() => setFilterEntity(key)}
+              className={`text-sm font-medium transition-colors ${
+                filterEntity === key
+                  ? 'text-foreground'
+                  : 'text-muted-foreground hover:text-foreground'
+              }`}
+            >
+              {key === 'all' ? t('customFields.filterAll') : entityTypeLabels[key]}
+              <span className="ml-1 text-xs text-muted-foreground">
+                ({key === 'all' ? fields.length : fields.filter((f) => f.entityType === key).length}
+                )
+              </span>
+            </button>
+          ))}
         </div>
         </div>
-        <Button onClick={openCreate}>
-          <Plus className="mr-1 h-4 w-4" /> {t('customFields.addField')}
+        <Button size="sm" onClick={openCreate}>
+          <Plus className="mr-1 h-3.5 w-3.5" /> {t('customFields.addField')}
         </Button>
         </Button>
       </div>
       </div>
 
 
-      <div className="flex gap-2">
-        {["all", "service_record", "quote"].map((key) => (
-          <Button
-            key={key}
-            variant={filterEntity === key ? "default" : "outline"}
-            size="sm"
-            onClick={() => setFilterEntity(key)}
-          >
-            {key === "all" ? t('customFields.filterAll') : entityTypeLabels[key]}
-          </Button>
-        ))}
-      </div>
-
       {filtered.length === 0 ? (
       {filtered.length === 0 ? (
-        <Card className="border-0 shadow-sm">
-          <CardContent className="py-12 text-center text-muted-foreground">
-            {t('customFields.emptyState')}
-          </CardContent>
-        </Card>
+        <div className="rounded-lg border border-dashed py-8 text-center text-sm text-muted-foreground">
+          {t('customFields.emptyState')}
+        </div>
       ) : (
       ) : (
-        <div className="space-y-2">
-          {filtered.map((field) => (
-            <Card key={field.id} className="border-0 shadow-sm">
-              <CardContent className="flex items-center gap-4 py-3">
-                <GripVertical className="h-4 w-4 text-muted-foreground/50" />
+        <div className="rounded-lg border divide-y">
+          {filtered.map((field) => {
+            const sectionName = layoutConfig ? getSectionForField(field.id, layoutConfig) : null
+            return (
+              <div
+                key={field.id}
+                className="flex items-center gap-3 px-3 py-2 hover:bg-muted/50 transition-colors cursor-pointer"
+                onClick={() => openEdit(field)}
+              >
                 <div className="min-w-0 flex-1">
                 <div className="min-w-0 flex-1">
-                  <div className="flex items-center gap-2">
-                    <span className="font-medium">{field.label}</span>
-                    <Badge variant="outline" className="text-xs">
+                  <div className="flex items-center gap-1.5">
+                    <span className="text-sm font-medium">{field.label}</span>
+                    <span className="text-xs text-muted-foreground">·</span>
+                    <span className="text-xs text-muted-foreground">
                       {fieldTypeLabels[field.fieldType] || field.fieldType}
                       {fieldTypeLabels[field.fieldType] || field.fieldType}
-                    </Badge>
-                    <Badge variant="outline" className="text-xs">
-                      {entityTypeLabels[field.entityType]}
-                    </Badge>
-                    {field.required && (
-                      <Badge variant="outline" className="text-xs text-amber-600 border-amber-500/20 bg-amber-500/10">
-                        {t('customFields.required')}
-                      </Badge>
-                    )}
+                    </span>
+                    {field.required && <span className="text-xs text-amber-600">*</span>}
                     {!field.isActive && (
                     {!field.isActive && (
-                      <Badge variant="outline" className="text-xs text-gray-500">
+                      <Badge variant="outline" className="h-4 px-1 text-[10px] text-gray-500">
                         {t('customFields.inactive')}
                         {t('customFields.inactive')}
                       </Badge>
                       </Badge>
                     )}
                     )}
                   </div>
                   </div>
-                  <p className="text-xs text-muted-foreground font-mono">{field.name}</p>
+                  <div className="flex items-center gap-1.5">
+                    <span className="text-[11px] text-muted-foreground font-mono">
+                      {field.name}
+                    </span>
+                    <span className="text-[11px] text-muted-foreground">·</span>
+                    <span className="text-[11px] text-muted-foreground">
+                      {entityTypeLabels[field.entityType]}
+                    </span>
+                    {layoutConfig && (
+                      <>
+                        <span className="text-[11px] text-muted-foreground">·</span>
+                        {sectionName ? (
+                          <span className="text-[11px] text-blue-600">{sectionName}</span>
+                        ) : (
+                          <span className="text-[11px] text-muted-foreground italic">
+                            unassigned
+                          </span>
+                        )}
+                      </>
+                    )}
+                  </div>
                 </div>
                 </div>
-                <div className="flex items-center gap-1">
-                  <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => openEdit(field)}>
-                    <Pencil className="h-3.5 w-3.5" />
+                <div
+                  className="flex items-center gap-0.5 shrink-0"
+                  onClick={(e) => e.stopPropagation()}
+                >
+                  <Button
+                    variant="ghost"
+                    size="icon"
+                    className="h-7 w-7"
+                    onClick={() => openEdit(field)}
+                  >
+                    <Pencil className="h-3 w-3" />
                   </Button>
                   </Button>
                   <Button
                   <Button
                     variant="ghost"
                     variant="ghost"
                     size="icon"
                     size="icon"
-                    className="h-8 w-8 text-muted-foreground hover:text-destructive"
+                    className="h-7 w-7 text-muted-foreground hover:text-destructive"
                     onClick={() => handleDelete(field)}
                     onClick={() => handleDelete(field)}
                   >
                   >
-                    <Trash2 className="h-3.5 w-3.5" />
+                    <Trash2 className="h-3 w-3" />
                   </Button>
                   </Button>
                 </div>
                 </div>
-              </CardContent>
-            </Card>
-          ))}
+              </div>
+            )
+          })}
         </div>
         </div>
       )}
       )}
 
 
       {/* Create/Edit Dialog */}
       {/* Create/Edit Dialog */}
-      <Dialog open={showDialog} onOpenChange={(open) => { setShowDialog(open); if (!open) resetForm(); }}>
+      <Dialog
+        open={showDialog}
+        onOpenChange={(open) => {
+          setShowDialog(open)
+          if (!open) resetForm()
+        }}
+      >
         <DialogContent className="max-w-md">
         <DialogContent className="max-w-md">
           <DialogHeader>
           <DialogHeader>
-            <DialogTitle>{editing ? t('customFields.editField') : t('customFields.newField')}</DialogTitle>
+            <DialogTitle>
+              {editing ? t('customFields.editField') : t('customFields.newField')}
+            </DialogTitle>
           </DialogHeader>
           </DialogHeader>
           <div className="space-y-4">
           <div className="space-y-4">
             <div className="space-y-2">
             <div className="space-y-2">
@@ -256,8 +310,13 @@ export function CustomFieldsManager({
                   setFormData({
                   setFormData({
                     ...formData,
                     ...formData,
                     label: e.target.value,
                     label: e.target.value,
-                    name: editing ? formData.name : e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""),
-                  });
+                    name: editing
+                      ? formData.name
+                      : e.target.value
+                          .toLowerCase()
+                          .replace(/[^a-z0-9]+/g, '_')
+                          .replace(/^_|_$/g, ''),
+                  })
                 }}
                 }}
               />
               />
             </div>
             </div>
@@ -281,14 +340,20 @@ export function CustomFieldsManager({
                   value={formData.fieldType}
                   value={formData.fieldType}
                   onValueChange={(v) => setFormData({ ...formData, fieldType: v as FieldType })}
                   onValueChange={(v) => setFormData({ ...formData, fieldType: v as FieldType })}
                 >
                 >
-                  <SelectTrigger><SelectValue /></SelectTrigger>
+                  <SelectTrigger>
+                    <SelectValue />
+                  </SelectTrigger>
                   <SelectContent>
                   <SelectContent>
                     <SelectItem value="text">{t('customFields.fieldTypes.text')}</SelectItem>
                     <SelectItem value="text">{t('customFields.fieldTypes.text')}</SelectItem>
                     <SelectItem value="number">{t('customFields.fieldTypes.number')}</SelectItem>
                     <SelectItem value="number">{t('customFields.fieldTypes.number')}</SelectItem>
                     <SelectItem value="date">{t('customFields.fieldTypes.date')}</SelectItem>
                     <SelectItem value="date">{t('customFields.fieldTypes.date')}</SelectItem>
                     <SelectItem value="select">{t('customFields.fieldTypes.select')}</SelectItem>
                     <SelectItem value="select">{t('customFields.fieldTypes.select')}</SelectItem>
-                    <SelectItem value="checkbox">{t('customFields.fieldTypes.checkbox')}</SelectItem>
-                    <SelectItem value="textarea">{t('customFields.fieldTypes.textarea')}</SelectItem>
+                    <SelectItem value="checkbox">
+                      {t('customFields.fieldTypes.checkbox')}
+                    </SelectItem>
+                    <SelectItem value="textarea">
+                      {t('customFields.fieldTypes.textarea')}
+                    </SelectItem>
                   </SelectContent>
                   </SelectContent>
                 </Select>
                 </Select>
               </div>
               </div>
@@ -300,16 +365,20 @@ export function CustomFieldsManager({
                   onValueChange={(v) => setFormData({ ...formData, entityType: v as EntityType })}
                   onValueChange={(v) => setFormData({ ...formData, entityType: v as EntityType })}
                   disabled={!!editing}
                   disabled={!!editing}
                 >
                 >
-                  <SelectTrigger><SelectValue /></SelectTrigger>
+                  <SelectTrigger>
+                    <SelectValue />
+                  </SelectTrigger>
                   <SelectContent>
                   <SelectContent>
-                    <SelectItem value="service_record">{t('customFields.serviceRecord')}</SelectItem>
+                    <SelectItem value="service_record">
+                      {t('customFields.serviceRecord')}
+                    </SelectItem>
                     <SelectItem value="quote">{t('customFields.quote')}</SelectItem>
                     <SelectItem value="quote">{t('customFields.quote')}</SelectItem>
                   </SelectContent>
                   </SelectContent>
                 </Select>
                 </Select>
               </div>
               </div>
             </div>
             </div>
 
 
-            {formData.fieldType === "select" && (
+            {formData.fieldType === 'select' && (
               <div className="space-y-2">
               <div className="space-y-2">
                 <Label>{t('customFields.optionsLabel')}</Label>
                 <Label>{t('customFields.optionsLabel')}</Label>
                 <Input
                 <Input
@@ -338,7 +407,13 @@ export function CustomFieldsManager({
             </div>
             </div>
 
 
             <div className="flex justify-end gap-2">
             <div className="flex justify-end gap-2">
-              <Button variant="ghost" onClick={() => { setShowDialog(false); resetForm(); }}>
+              <Button
+                variant="ghost"
+                onClick={() => {
+                  setShowDialog(false)
+                  resetForm()
+                }}
+              >
                 {t('customFields.cancel')}
                 {t('customFields.cancel')}
               </Button>
               </Button>
               <Button onClick={handleSave} disabled={loading || !formData.name || !formData.label}>
               <Button onClick={handleSave} disabled={loading || !formData.name || !formData.label}>
@@ -350,5 +425,5 @@ export function CustomFieldsManager({
         </DialogContent>
         </DialogContent>
       </Dialog>
       </Dialog>
     </div>
     </div>
-  );
+  )
 }
 }

+ 440 - 262
src/features/quotes/Components/QuotePDF.tsx

@@ -1,8 +1,12 @@
+import React from 'react'
 import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
 import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
 import { formatCurrency, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { formatCurrency, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { createStyles, gray, getFontBold } from '@/features/vehicles/Components/invoice-pdf/styles'
 import { createStyles, gray, getFontBold } from '@/features/vehicles/Components/invoice-pdf/styles'
 import { HtmlToPdf } from '@/features/vehicles/Components/invoice-pdf/Notes'
 import { HtmlToPdf } from '@/features/vehicles/Components/invoice-pdf/Notes'
+import { CustomFields } from '@/features/vehicles/Components/invoice-pdf/CustomFields'
 import type { TemplateConfig } from '@/features/vehicles/Components/invoice-pdf/types'
 import type { TemplateConfig } from '@/features/vehicles/Components/invoice-pdf/types'
+import type { InvoiceLayoutConfig, InvoiceSection } from '@/features/settings/Schema/invoiceLayoutSchema'
+import { isCustomFieldId, fromCustomFieldId, groupSectionsForRendering, getDefaultInvoiceLayout, getOrderedFieldIds } from '@/features/settings/Schema/invoiceLayoutSchema'
 
 
 function fillTemplate(template: string, values: Record<string, string>): string {
 function fillTemplate(template: string, values: Record<string, string>): string {
   return Object.entries(values).reduce(
   return Object.entries(values).reduce(
@@ -81,7 +85,9 @@ export function QuotePDF({
   imageAttachments = [],
   imageAttachments = [],
   otherAttachments = [],
   otherAttachments = [],
   pdfAttachmentNames = [],
   pdfAttachmentNames = [],
+  customFields = [],
   labels = {},
   labels = {},
+  layoutConfig,
 }: {
 }: {
   data: QuoteData
   data: QuoteData
   workshop?: WorkshopInfo
   workshop?: WorkshopInfo
@@ -95,16 +101,48 @@ export function QuotePDF({
   imageAttachments?: ImageAttachmentPDF[]
   imageAttachments?: ImageAttachmentPDF[]
   otherAttachments?: OtherAttachmentPDF[]
   otherAttachments?: OtherAttachmentPDF[]
   pdfAttachmentNames?: string[]
   pdfAttachmentNames?: string[]
+  customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>
   labels?: Record<string, string>
   labels?: Record<string, string>
+  layoutConfig?: InvoiceLayoutConfig
 }) {
 }) {
   const primaryColor = template?.primaryColor || '#d97706'
   const primaryColor = template?.primaryColor || '#d97706'
   const fontFamily = template?.fontFamily || 'Helvetica'
   const fontFamily = template?.fontFamily || 'Helvetica'
   const headerStyle = template?.headerStyle || 'standard'
   const headerStyle = template?.headerStyle || 'standard'
   const showLogo = template?.showLogo !== false
   const showLogo = template?.showLogo !== false
   const showCompanyName = template?.showCompanyName !== false
   const showCompanyName = template?.showCompanyName !== false
+  const logoScale = (template?.logoSize || 100) / 100
   const styles = createStyles(primaryColor, fontFamily)
   const styles = createStyles(primaryColor, fontFamily)
   const fontBold = getFontBold(fontFamily)
   const fontBold = getFontBold(fontFamily)
 
 
+  // Layout config helpers
+  const sections = layoutConfig?.sections ?? []
+  const sectionMap = new Map<string, InvoiceSection>(sections.map((s) => [s.id, s]))
+
+  const isSectionVisible = (id: string): boolean => {
+    const s = sectionMap.get(id)
+    return s ? s.visible : true // default visible when no config
+  }
+
+  const getVisibleFieldsForSection = (sectionId: string): Set<string> | null => {
+    const section = sectionMap.get(sectionId)
+    if (!section?.fields) return null
+    return new Set(section.fields.filter((f) => f.visible).map((f) => f.id))
+  }
+
+  const getCustomFieldsForSection = (
+    sectionId: string,
+  ): Array<{ fieldId: string; label: string; value: string; fieldType: string }> => {
+    if (!layoutConfig || !customFields?.length) return []
+    const section = sectionMap.get(sectionId)
+    if (!section?.fields) return []
+    const cfIds = new Set(
+      section.fields
+        .filter(f => f.visible && isCustomFieldId(f.id))
+        .map(f => fromCustomFieldId(f.id))
+    )
+    return customFields.filter(cf => cfIds.has(cf.fieldId))
+  }
+
   const quoteNum = data.quoteNumber || 'QUOTE'
   const quoteNum = data.quoteNumber || 'QUOTE'
   const df = dateFormat || DEFAULT_DATE_FORMAT
   const df = dateFormat || DEFAULT_DATE_FORMAT
   const tz = timezone || undefined
   const tz = timezone || undefined
@@ -124,22 +162,20 @@ export function QuotePDF({
           borderBottomColor: '#e5e7eb',
           borderBottomColor: '#e5e7eb',
         }}
         }}
       >
       >
-        <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+        <View>
           {showLogo && logoDataUri && (
           {showLogo && logoDataUri && (
             <Image
             <Image
               src={logoDataUri}
               src={logoDataUri}
-              style={{ maxWidth: 40, maxHeight: 40, borderRadius: 4, objectFit: 'contain' }}
+              style={{ maxWidth: 40 * logoScale, maxHeight: 40 * logoScale, borderRadius: 4, objectFit: 'contain', marginBottom: 4 }}
             />
             />
           )}
           )}
           {showCompanyName && (
           {showCompanyName && (
-            <View>
-              <Text style={{ fontSize: 16, fontFamily: fontBold, color: primaryColor }}>
-                {shopName}
-              </Text>
-              {workshop?.address && (
-                <Text style={{ fontSize: 8, color: gray }}>{workshop.address}</Text>
-              )}
-            </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' }}>
         <View style={{ alignItems: 'flex-end' }}>
@@ -185,43 +221,34 @@ export function QuotePDF({
           marginHorizontal: -10,
           marginHorizontal: -10,
         }}
         }}
       >
       >
-        <View
-          style={{
-            flexDirection: 'row',
-            alignItems: 'center',
-            justifyContent: 'center',
-            gap: 12,
-          }}
-        >
+        <View style={{ alignItems: 'center' }}>
           {showLogo && logoDataUri && (
           {showLogo && logoDataUri && (
             <Image
             <Image
               src={logoDataUri}
               src={logoDataUri}
-              style={{ maxWidth: 50, maxHeight: 50, borderRadius: 4, objectFit: 'contain' }}
+              style={{ maxWidth: 50 * logoScale, maxHeight: 50 * logoScale, borderRadius: 4, objectFit: 'contain', marginBottom: 6 }}
             />
             />
           )}
           )}
-          <View style={{ alignItems: 'center' }}>
-            {showCompanyName && (
-              <Text style={{ fontSize: 22, fontFamily: fontBold, color: 'white' }}>
-                {shopName}
+          {showCompanyName && (
+            <Text style={{ fontSize: 22, fontFamily: fontBold, color: 'white' }}>
+              {shopName}
+            </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)' }}>
+                {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`}
               </Text>
               </Text>
             )}
             )}
-            {workshop?.address && (
-              <Text style={{ fontSize: 9, color: 'rgba(255,255,255,0.8)', marginTop: 2 }}>
-                {workshop.address}
+            {workshop?.email && (
+              <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                {workshop.email}
               </Text>
               </Text>
             )}
             )}
-            <View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
-              {workshop?.phone && (
-                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
-                  {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`}
-                </Text>
-              )}
-              {workshop?.email && (
-                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
-                  {workshop.email}
-                </Text>
-              )}
-            </View>
           </View>
           </View>
         </View>
         </View>
         {torqvoiceLogoDataUri && (
         {torqvoiceLogoDataUri && (
@@ -266,7 +293,14 @@ export function QuotePDF({
         {showLogo && logoDataUri && (
         {showLogo && logoDataUri && (
           <Image
           <Image
             src={logoDataUri}
             src={logoDataUri}
-            style={{ width: 60, height: 60, marginBottom: 6, borderRadius: 4 }}
+            style={{
+              maxWidth: 150 * logoScale,
+              maxHeight: 60 * logoScale,
+              marginBottom: 6,
+              borderRadius: 4,
+              objectFit: 'contain',
+              objectPosition: 'left',
+            }}
           />
           />
         )}
         )}
         {showCompanyName && <Text style={styles.brandName}>{shopName}</Text>}
         {showCompanyName && <Text style={styles.brandName}>{shopName}</Text>}
@@ -289,252 +323,396 @@ export function QuotePDF({
     </View>
     </View>
   )
   )
 
 
-  return (
-    <Document>
-      <Page size="A4" style={styles.page}>
-        {headerStyle === 'compact'
-          ? renderCompactHeader()
-          : headerStyle === 'modern'
-            ? renderModernHeader()
-            : renderStandardHeader()}
-
-        <View style={styles.infoRow}>
-          {data.customer && (
-            <View style={styles.infoBox}>
-              <Text style={styles.infoLabel}>{labels.to || 'To'}</Text>
-              <Text style={styles.infoTextBold}>{data.customer.name}</Text>
-              {data.customer.company && (
-                <Text style={styles.infoText}>{data.customer.company}</Text>
-              )}
-              {data.customer.address && (
-                <Text style={styles.infoTextSmall}>{data.customer.address}</Text>
-              )}
-              {data.customer.email && (
-                <Text style={styles.infoTextSmall}>{data.customer.email}</Text>
-              )}
-              {data.customer.phone && (
-                <Text style={styles.infoTextSmall}>{data.customer.phone}</Text>
-              )}
-            </View>
-          )}
-          {data.vehicle && (
-            <View style={styles.infoBox}>
-              <Text style={styles.infoLabel}>{labels.vehicle || 'Vehicle'}</Text>
-              <Text style={styles.infoTextBold}>
-                {data.vehicle.year} {data.vehicle.make} {data.vehicle.model}
+  // ---------- Section renderers ----------
+
+  const customerCf = getCustomFieldsForSection('customer')
+  const vehicleCf = getCustomFieldsForSection('vehicle')
+  const serviceCf = getCustomFieldsForSection('service')
+  const generalCf = getCustomFieldsForSection('general')
+  const generalFallbackCf = !layoutConfig ? customFields : generalCf
+
+  const visibleCustomerFields = getVisibleFieldsForSection('customer')
+  const visibleVehicleFields = getVisibleFieldsForSection('vehicle')
+  const visibleServiceFields = getVisibleFieldsForSection('service')
+
+  const renderCustomerSection = () => {
+    const showC = (fid: string) => !visibleCustomerFields || visibleCustomerFields.has(fid)
+    const hasCustomer = data.customer && (showC('customer_name') || (showC('customer_company') && data.customer.company) || (showC('customer_address') && data.customer.address) || (showC('customer_email') && data.customer.email) || (showC('customer_phone') && data.customer.phone))
+    const hasCustCf = customerCf.some(cf => cf.value !== '' && cf.value != null)
+    if (!hasCustomer && !hasCustCf) return null
+
+    const fieldOrder = getOrderedFieldIds(visibleCustomerFields, ['customer_name', 'customer_company', 'customer_address', 'customer_email', 'customer_phone'])
+    const c = data.customer
+
+    const renderField = (fid: string) => {
+      if (!c || !showC(fid)) return null
+      switch (fid) {
+        case 'customer_name': return <Text key={fid} style={styles.infoTextBold}>{c.name}</Text>
+        case 'customer_company': return c.company ? <Text key={fid} style={styles.infoText}>{c.company}</Text> : null
+        case 'customer_address': return c.address ? <Text key={fid} style={styles.infoTextSmall}>{c.address}</Text> : null
+        case 'customer_email': return c.email ? <Text key={fid} style={styles.infoTextSmall}>{c.email}</Text> : null
+        case 'customer_phone': return c.phone ? <Text key={fid} style={styles.infoTextSmall}>{c.phone}</Text> : null
+        default: return null
+      }
+    }
+
+    return (
+      <View style={styles.infoBox}>
+        <Text style={styles.infoLabel}>{labels.to || 'To'}</Text>
+        {fieldOrder.map(renderField)}
+        {customerCf.filter(cf => cf.value !== '' && cf.value != null).map((cf, i) => (
+          <Text key={`cf-cust-${i}`} style={styles.infoTextSmall}>{cf.label}: {cf.value}</Text>
+        ))}
+      </View>
+    )
+  }
+
+  const renderVehicleSection = () => {
+    const showV = (fid: string) => !visibleVehicleFields || visibleVehicleFields.has(fid)
+    const hasVehicle = data.vehicle && (showV('vehicle_name') || (showV('vin') && data.vehicle.vin) || (showV('license_plate') && data.vehicle.licensePlate))
+    const hasVehCf = vehicleCf.some(cf => cf.value !== '' && cf.value != null)
+    if (!hasVehicle && !hasVehCf) return null
+
+    const fieldOrder = getOrderedFieldIds(visibleVehicleFields, ['vehicle_name', 'vin', 'license_plate'])
+    const v = data.vehicle
+
+    const renderField = (fid: string) => {
+      if (!v || !showV(fid)) return null
+      switch (fid) {
+        case 'vehicle_name': return <Text key={fid} style={styles.infoTextBold}>{v.year} {v.make} {v.model}</Text>
+        case 'vin': return v.vin ? <Text key={fid} style={styles.infoTextSmall}>{labels.vin ? fillTemplate(labels.vin, { vin: v.vin }) : `VIN: ${v.vin}`}</Text> : null
+        case 'license_plate': return v.licensePlate ? <Text key={fid} style={styles.infoTextSmall}>{labels.plate ? fillTemplate(labels.plate, { plate: v.licensePlate }) : `Plate: ${v.licensePlate}`}</Text> : null
+        default: return null
+      }
+    }
+
+    return (
+      <View style={styles.infoBox}>
+        <Text style={styles.infoLabel}>{labels.vehicle || 'Vehicle'}</Text>
+        {fieldOrder.map(renderField)}
+        {vehicleCf.filter(cf => cf.value !== '' && cf.value != null).map((cf, i) => (
+          <Text key={`cf-veh-${i}`} style={styles.infoTextSmall}>{cf.label}: {cf.value}</Text>
+        ))}
+      </View>
+    )
+  }
+
+  const renderServiceSection = () => {
+    const show = (fid: string) => !visibleServiceFields || visibleServiceFields.has(fid)
+    const hasBuiltin = show('service_title')
+    const hasCf = serviceCf.some(cf => cf.value !== '' && cf.value != null)
+    if (!hasBuiltin && !hasCf) return null
+
+    const fieldOrder = getOrderedFieldIds(visibleServiceFields, ['service_title'])
+
+    const renderField = (fid: string) => {
+      if (!show(fid)) return null
+      switch (fid) {
+        case 'service_title': return <Text key={fid} style={styles.infoTextBold}>{data.title}</Text>
+        default: return null
+      }
+    }
+
+    return (
+      <View style={styles.infoBox}>
+        <Text style={styles.infoLabel}>{labels.quoteDetails || 'Quote Details'}</Text>
+        {fieldOrder.map(renderField)}
+        {serviceCf.filter(cf => cf.value !== '' && cf.value != null).map((cf, i) => (
+          <Text key={`cf-${i}`} style={styles.infoTextSmall}>{cf.label}: {cf.value}</Text>
+        ))}
+      </View>
+    )
+  }
+
+  const renderPartsTable = () => {
+    if (data.partItems.length === 0) return null
+    return (
+      <View>
+        <Text style={styles.sectionTitle}>{labels.parts || 'Parts'}</Text>
+        <View style={styles.table}>
+          <View style={styles.tableHeader}>
+            <Text style={{ ...styles.tableHeaderCell, width: '15%' }}>{labels.partNumber || 'Part #'}</Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '35%' }}>{labels.description || 'Description'}</Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '12%', textAlign: 'right' }}>
+              {labels.qty || 'Qty'}
+            </Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '18%', textAlign: 'right' }}>
+              {labels.unitPrice || 'Unit Price'}
+            </Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
+              {labels.total || 'Total'}
+            </Text>
+          </View>
+          {data.partItems.map((p, i) => (
+            <View key={i} style={{ ...styles.tableRow, ...(p.excluded ? { opacity: 0.5 } : {}) }}>
+              <Text style={{ ...styles.tableCell, width: '15%', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>{p.partNumber || '-'}</Text>
+              <Text style={{ ...styles.tableCell, width: '35%', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>{p.name}</Text>
+              <Text style={{ ...styles.tableCell, width: '12%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {p.quantity}
+              </Text>
+              <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {formatCurrency(p.unitPrice, currencyCode)}
+              </Text>
+              <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {formatCurrency(p.total, currencyCode)}
               </Text>
               </Text>
-              {data.vehicle.vin && (
-                <Text style={styles.infoTextSmall}>{labels.vin ? fillTemplate(labels.vin, { vin: data.vehicle.vin }) : `VIN: ${data.vehicle.vin}`}</Text>
-              )}
-              {data.vehicle.licensePlate && (
-                <Text style={styles.infoTextSmall}>{labels.plate ? fillTemplate(labels.plate, { plate: data.vehicle.licensePlate }) : `Plate: ${data.vehicle.licensePlate}`}</Text>
-              )}
             </View>
             </View>
-          )}
-          <View style={styles.infoBox}>
-            <Text style={styles.infoLabel}>{labels.quoteDetails || 'Quote Details'}</Text>
-            <Text style={styles.infoTextBold}>{data.title}</Text>
-          </View>
+          ))}
         </View>
         </View>
+      </View>
+    )
+  }
 
 
-        {data.partItems.length > 0 && (
-          <View>
-            <Text style={styles.sectionTitle}>{labels.parts || 'Parts'}</Text>
-            <View style={styles.table}>
-              <View style={styles.tableHeader}>
-                <Text style={{ ...styles.tableHeaderCell, width: '15%' }}>{labels.partNumber || 'Part #'}</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '35%' }}>{labels.description || 'Description'}</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '12%', textAlign: 'right' }}>
-                  {labels.qty || 'Qty'}
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '18%', textAlign: 'right' }}>
-                  {labels.unitPrice || 'Unit Price'}
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  {labels.total || 'Total'}
-                </Text>
-              </View>
-              {data.partItems.map((p, i) => (
-                <View key={i} style={{ ...styles.tableRow, ...(p.excluded ? { opacity: 0.5 } : {}) }}>
-                  <Text style={{ ...styles.tableCell, width: '15%', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>{p.partNumber || '-'}</Text>
-                  <Text style={{ ...styles.tableCell, width: '35%', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>{p.name}</Text>
-                  <Text style={{ ...styles.tableCell, width: '12%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {p.quantity}
-                  </Text>
-                  <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {formatCurrency(p.unitPrice, currencyCode)}
-                  </Text>
-                  <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {formatCurrency(p.total, currencyCode)}
-                  </Text>
-                </View>
-              ))}
-            </View>
+  const renderLaborTable = () => {
+    if (data.laborItems.length === 0) return null
+    return (
+      <View>
+        <Text style={styles.sectionTitle}>{labels.labor || 'Labor'}</Text>
+        <View style={styles.table}>
+          <View style={styles.tableHeader}>
+            <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.description || 'Description'}</Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>
+              {labels.hours || 'Hours'}
+            </Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
+              {labels.rate || 'Rate'}
+            </Text>
+            <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
+              {labels.total || 'Total'}
+            </Text>
           </View>
           </View>
-        )}
+          {data.laborItems.map((l, i) => (
+            <View key={i} style={{ ...styles.tableRow, ...(l.excluded ? { opacity: 0.5 } : {}) }}>
+              <Text style={{ ...styles.tableCell, width: '45%', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>{l.description}</Text>
+              <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {l.hours}
+              </Text>
+              <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {labels.ratePerHour ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(l.rate, currencyCode) }) : `${formatCurrency(l.rate, currencyCode)}/hr`}
+              </Text>
+              <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                {formatCurrency(l.total, currencyCode)}
+              </Text>
+            </View>
+          ))}
+        </View>
+      </View>
+    )
+  }
 
 
+  const renderTotals = () => {
+    const laborTotal = data.laborItems.reduce((sum, l) => l.excluded ? sum : sum + l.total, 0)
+    const partsTotal = data.partItems.reduce((sum, p) => p.excluded ? sum : sum + p.total, 0)
+    const sub = laborTotal + partsTotal
+    const disc = data.discountType === 'percentage'
+      ? sub * (data.discountValue / 100)
+      : data.discountType === 'fixed'
+      ? Math.min(data.discountValue, sub)
+      : 0
+    const tax = (sub - disc) * (data.taxRate / 100)
+    const total = sub - disc + tax
+    return (
+      <View style={styles.totalsBox}>
         {data.laborItems.length > 0 && (
         {data.laborItems.length > 0 && (
-          <View>
-            <Text style={styles.sectionTitle}>{labels.labor || 'Labor'}</Text>
-            <View style={styles.table}>
-              <View style={styles.tableHeader}>
-                <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.description || 'Description'}</Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>
-                  {labels.hours || 'Hours'}
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  {labels.rate || 'Rate'}
-                </Text>
-                <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
-                  {labels.total || 'Total'}
-                </Text>
-              </View>
-              {data.laborItems.map((l, i) => (
-                <View key={i} style={{ ...styles.tableRow, ...(l.excluded ? { opacity: 0.5 } : {}) }}>
-                  <Text style={{ ...styles.tableCell, width: '45%', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>{l.description}</Text>
-                  <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {l.hours}
-                  </Text>
-                  <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {labels.ratePerHour ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(l.rate, currencyCode) }) : `${formatCurrency(l.rate, currencyCode)}/hr`}
-                  </Text>
-                  <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                    {formatCurrency(l.total, currencyCode)}
-                  </Text>
-                </View>
-              ))}
-            </View>
+          <View style={styles.totalRow}>
+            <Text style={styles.totalLabel}>{labels.labor || 'Labor'}</Text>
+            <Text style={styles.totalValue}>
+              {formatCurrency(laborTotal, currencyCode)}
+            </Text>
           </View>
           </View>
         )}
         )}
-
-        {(() => {
-          const laborTotal = data.laborItems.reduce((sum, l) => l.excluded ? sum : sum + l.total, 0)
-          const partsTotal = data.partItems.reduce((sum, p) => p.excluded ? sum : sum + p.total, 0)
-          const sub = laborTotal + partsTotal
-          const disc = data.discountType === 'percentage'
-            ? sub * (data.discountValue / 100)
-            : data.discountType === 'fixed'
-            ? Math.min(data.discountValue, sub)
-            : 0
-          const tax = (sub - disc) * (data.taxRate / 100)
-          const total = sub - disc + tax
-          return (
-            <View style={styles.totalsBox}>
-              {data.laborItems.length > 0 && (
-                <View style={styles.totalRow}>
-                  <Text style={styles.totalLabel}>{labels.labor || 'Labor'}</Text>
-                  <Text style={styles.totalValue}>
-                    {formatCurrency(laborTotal, currencyCode)}
-                  </Text>
-                </View>
-              )}
-              {data.partItems.length > 0 && (
-                <View style={styles.totalRow}>
-                  <Text style={styles.totalLabel}>{labels.parts || 'Parts'}</Text>
-                  <Text style={styles.totalValue}>
-                    {formatCurrency(partsTotal, currencyCode)}
-                  </Text>
-                </View>
-              )}
-              {sub > 0 && (
-                <View style={styles.totalRow}>
-                  <Text style={styles.totalLabel}>{labels.subtotal || 'Subtotal'}</Text>
-                  <Text style={styles.totalValue}>{formatCurrency(sub, currencyCode)}</Text>
-                </View>
-              )}
-              {disc > 0 && (
-                <View style={styles.totalRow}>
-                  <Text style={styles.totalLabel}>
-                    {data.discountType === 'percentage'
-                      ? (labels.discountPercent ? fillTemplate(labels.discountPercent, { percent: String(data.discountValue) }) : `Discount (${data.discountValue}%)`)
-                      : (labels.discount || 'Discount')}
-                  </Text>
-                  <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
-                    {formatCurrency(-disc, currencyCode)}
-                  </Text>
-                </View>
-              )}
-              {data.taxRate > 0 && (
-                <View style={styles.totalRow}>
-                  <Text style={styles.totalLabel}>{labels.tax ? fillTemplate(labels.tax, { rate: String(data.taxRate) }) : `Tax (${data.taxRate}%)`}</Text>
-                  <Text style={styles.totalValue}>{formatCurrency(tax, currencyCode)}</Text>
-                </View>
-              )}
-              <View style={styles.totalDivider} />
-              <View style={styles.grandTotalRow}>
-                <Text style={styles.grandTotalLabel}>{labels.total || 'Total'}</Text>
-                <Text style={styles.grandTotalValue}>
-                  {formatCurrency(total, currencyCode)}
-                </Text>
-              </View>
-            </View>
-          )
-        })()}
-
-        {torqvoiceLogoDataUri && (
-          <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginTop: 6 }}>
-            <Text style={{ fontSize: 7, color: gray }}>{labels.poweredBy || 'Powered by'}</Text>
-            <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
-            <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
+        {data.partItems.length > 0 && (
+          <View style={styles.totalRow}>
+            <Text style={styles.totalLabel}>{labels.parts || 'Parts'}</Text>
+            <Text style={styles.totalValue}>
+              {formatCurrency(partsTotal, currencyCode)}
+            </Text>
           </View>
           </View>
         )}
         )}
-
-        {data.description && (
-          <View style={styles.notesSection}>
-            <Text style={styles.notesLabel}>Description</Text>
-            <HtmlToPdf html={data.description} baseStyle={styles.notesText} fontBold={fontBold} />
+        {sub > 0 && (
+          <View style={styles.totalRow}>
+            <Text style={styles.totalLabel}>{labels.subtotal || 'Subtotal'}</Text>
+            <Text style={styles.totalValue}>{formatCurrency(sub, currencyCode)}</Text>
           </View>
           </View>
         )}
         )}
-
-        {portalUrl && (
-          <View style={{ marginTop: 8 }}>
-            <Text style={{ fontSize: 8, color: gray, textAlign: 'center' }}>
-              {labels.viewPortal ? fillTemplate(labels.viewPortal, { url: portalUrl }) : `View your portal: ${portalUrl}`}
+        {disc > 0 && (
+          <View style={styles.totalRow}>
+            <Text style={styles.totalLabel}>
+              {data.discountType === 'percentage'
+                ? (labels.discountPercent ? fillTemplate(labels.discountPercent, { percent: String(data.discountValue) }) : `Discount (${data.discountValue}%)`)
+                : (labels.discount || 'Discount')}
+            </Text>
+            <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
+              {formatCurrency(-disc, currencyCode)}
             </Text>
             </Text>
           </View>
           </View>
         )}
         )}
-
-        {/* Document attachment names */}
-        {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
-          <View style={{ marginTop: 10 }}>
-            <Text style={styles.sectionTitle}>{labels.attachedDocuments || 'Attached Documents'}</Text>
-            {otherAttachments.map((att, i) => (
-              <Text key={`other-${i}`} style={{ fontSize: 9, color: gray, marginBottom: 2 }}>
-                {att.fileName}
-              </Text>
-            ))}
-            {pdfAttachmentNames.map((name, i) => (
-              <Text key={`pdf-${i}`} style={{ fontSize: 9, color: gray, marginBottom: 2 }}>
-                {labels.attached ? fillTemplate(labels.attached, { name }) : `${name} (attached)`}
-              </Text>
-            ))}
+        {data.taxRate > 0 && (
+          <View style={styles.totalRow}>
+            <Text style={styles.totalLabel}>{labels.tax ? fillTemplate(labels.tax, { rate: String(data.taxRate) }) : `Tax (${data.taxRate}%)`}</Text>
+            <Text style={styles.totalValue}>{formatCurrency(tax, currencyCode)}</Text>
           </View>
           </View>
         )}
         )}
+        <View style={styles.totalDivider} />
+        <View style={styles.grandTotalRow}>
+          <Text style={styles.grandTotalLabel}>{labels.total || 'Total'}</Text>
+          <Text style={styles.grandTotalValue}>
+            {formatCurrency(total, currencyCode)}
+          </Text>
+        </View>
+      </View>
+    )
+  }
+
+  const renderGeneralSection = () => {
+    if (generalFallbackCf.length === 0) return null
+    return (
+      <View style={{ marginTop: 10 }}>
+        <CustomFields fields={generalFallbackCf} styles={styles} labels={labels} />
+      </View>
+    )
+  }
+
+  const renderNotes = () => {
+    if (!data.description) return null
+    return (
+      <View style={styles.notesSection}>
+        <Text style={styles.notesLabel}>Description</Text>
+        <HtmlToPdf html={data.description} baseStyle={styles.notesText} fontBold={fontBold} />
+      </View>
+    )
+  }
+
+  const renderFooter = () => (
+    <>
+      {portalUrl && (
+        <View style={{ marginTop: 8 }}>
+          <Text style={{ fontSize: 8, color: gray, textAlign: 'center' }}>
+            {labels.viewPortal ? fillTemplate(labels.viewPortal, { url: portalUrl }) : `View your portal: ${portalUrl}`}
+          </Text>
+        </View>
+      )}
 
 
-        {torqvoiceLogoDataUri ? (
-          <View style={{
-            ...styles.footer,
-            flexDirection: 'row',
-            alignItems: 'center',
-            justifyContent: 'center',
-            gap: 4,
-          }}>
-            <Text style={{ fontSize: 8, color: gray }}>
-              {validDate
-                ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`)
-                : (labels.validityFooter30 || 'This quote is valid for 30 days')} ·{' '}
+      {/* Document attachment names */}
+      {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
+        <View style={{ marginTop: 10 }}>
+          <Text style={styles.sectionTitle}>{labels.attachedDocuments || 'Attached Documents'}</Text>
+          {otherAttachments.map((att, i) => (
+            <Text key={`other-${i}`} style={{ fontSize: 9, color: gray, marginBottom: 2 }}>
+              {att.fileName}
             </Text>
             </Text>
-            <Text style={{ fontSize: 7, color: gray }}>{labels.poweredBy || '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}>
+          ))}
+          {pdfAttachmentNames.map((name, i) => (
+            <Text key={`pdf-${i}`} style={{ fontSize: 9, color: gray, marginBottom: 2 }}>
+              {labels.attached ? fillTemplate(labels.attached, { name }) : `${name} (attached)`}
+            </Text>
+          ))}
+        </View>
+      )}
+
+      {torqvoiceLogoDataUri ? (
+        <View style={{
+          ...styles.footer,
+          flexDirection: 'row',
+          alignItems: 'center',
+          justifyContent: 'center',
+          gap: 4,
+        }}>
+          <Text style={{ fontSize: 8, color: gray }}>
             {validDate
             {validDate
               ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`)
               ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`)
-              : (labels.validityFooter30 || 'This quote is valid for 30 days')} · {shopName}
+              : (labels.validityFooter30 || 'This quote is valid for 30 days')} ·{' '}
           </Text>
           </Text>
-        )}
+          <Text style={{ fontSize: 7, color: gray }}>{labels.poweredBy || '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}>
+          {validDate
+            ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`)
+            : (labels.validityFooter30 || 'This quote is valid for 30 days')} · {shopName}
+        </Text>
+      )}
+    </>
+  )
+
+  // ---------- Map section IDs to renderers ----------
+
+  const renderSection = (sectionId: string) => {
+    switch (sectionId) {
+      case 'header':
+        return headerStyle === 'compact'
+          ? renderCompactHeader()
+          : headerStyle === 'modern'
+            ? renderModernHeader()
+            : renderStandardHeader()
+      case 'customer':
+        return renderCustomerSection()
+      case 'vehicle':
+        return renderVehicleSection()
+      case 'service':
+        return renderServiceSection()
+      case 'parts_table':
+        return renderPartsTable()
+      case 'labor_table':
+        return renderLaborTable()
+      case 'totals':
+        return (
+          <>
+            {renderTotals()}
+            {torqvoiceLogoDataUri && (
+              <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginTop: 6 }}>
+                <Text style={{ fontSize: 7, color: gray }}>{labels.poweredBy || 'Powered by'}</Text>
+                <Image src={torqvoiceLogoDataUri} style={{ width: 12, height: 12 }} />
+                <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
+              </View>
+            )}
+          </>
+        )
+      case 'general':
+        return renderGeneralSection()
+      case 'notes':
+        return renderNotes()
+      case 'footer':
+        return renderFooter()
+      default:
+        return null
+    }
+  }
+
+  // Use column-based grouping from layout config
+  const effectiveSections = layoutConfig?.sections ?? getDefaultInvoiceLayout().sections
+  const renderGroups = groupSectionsForRendering(effectiveSections)
+  const renderedSections: React.ReactNode[] = []
+
+  for (const group of renderGroups) {
+    if (group.type === 'full-width') {
+      if (group.sectionId === 'footer') continue
+      renderedSections.push(
+        <View key={group.sectionId}>{renderSection(group.sectionId)}</View>
+      )
+    } else {
+      const allIds = [...group.left, ...group.right].filter(id => id !== 'footer')
+      if (allIds.length === 0) continue
+      renderedSections.push(
+        <View key={`col-${allIds[0]}`} style={styles.infoRow}>
+          <View style={{ flex: 1, gap: 4 }}>
+            {group.left.map(id => <React.Fragment key={id}>{renderSection(id)}</React.Fragment>)}
+          </View>
+          <View style={{ flex: 1, gap: 4 }}>
+            {group.right.map(id => <React.Fragment key={id}>{renderSection(id)}</React.Fragment>)}
+          </View>
+        </View>
+      )
+    }
+  }
+
+  return (
+    <Document>
+      <Page size="A4" style={styles.page}>
+        {renderedSections}
+        {renderFooter()}
       </Page>
       </Page>
 
 
       {imageAttachments.length > 0 && (
       {imageAttachments.length > 0 && (

+ 105 - 0
src/features/settings/Actions/invoiceLayoutActions.ts

@@ -0,0 +1,105 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { revalidatePath } from "next/cache";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import {
+  type InvoiceLayoutConfig,
+  invoiceLayoutConfigSchema,
+  mergeWithDefaults,
+  getDefaultInvoiceLayout,
+} from "@/features/settings/Schema/invoiceLayoutSchema";
+
+export async function getInvoiceLayoutConfig() {
+  return withAuth(async ({ organizationId }) => {
+    const setting = await db.appSetting.findUnique({
+      where: {
+        organizationId_key: {
+          organizationId,
+          key: SETTING_KEYS.INVOICE_LAYOUT_CONFIG,
+        },
+      },
+    });
+
+    if (!setting?.value) {
+      return getDefaultInvoiceLayout();
+    }
+
+    const parsed = JSON.parse(setting.value);
+    return mergeWithDefaults(parsed);
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.SETTINGS }] });
+}
+
+export async function saveInvoiceLayoutConfig(config: InvoiceLayoutConfig) {
+  return withAuth(async ({ userId, organizationId }) => {
+    const validated = invoiceLayoutConfigSchema.parse(config);
+    const value = JSON.stringify(validated);
+
+    await db.appSetting.upsert({
+      where: {
+        organizationId_key: {
+          organizationId,
+          key: SETTING_KEYS.INVOICE_LAYOUT_CONFIG,
+        },
+      },
+      update: { value },
+      create: {
+        userId,
+        organizationId,
+        key: SETTING_KEYS.INVOICE_LAYOUT_CONFIG,
+        value,
+      },
+    });
+
+    revalidatePath("/settings/templates");
+    return validated;
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+}
+
+export async function getQuoteLayoutConfig() {
+  return withAuth(async ({ organizationId }) => {
+    const setting = await db.appSetting.findUnique({
+      where: {
+        organizationId_key: {
+          organizationId,
+          key: SETTING_KEYS.QUOTE_LAYOUT_CONFIG,
+        },
+      },
+    });
+
+    if (!setting?.value) {
+      return getDefaultInvoiceLayout();
+    }
+
+    const parsed = JSON.parse(setting.value);
+    return mergeWithDefaults(parsed);
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.SETTINGS }] });
+}
+
+export async function saveQuoteLayoutConfig(config: InvoiceLayoutConfig) {
+  return withAuth(async ({ userId, organizationId }) => {
+    const validated = invoiceLayoutConfigSchema.parse(config);
+    const value = JSON.stringify(validated);
+
+    await db.appSetting.upsert({
+      where: {
+        organizationId_key: {
+          organizationId,
+          key: SETTING_KEYS.QUOTE_LAYOUT_CONFIG,
+        },
+      },
+      update: { value },
+      create: {
+        userId,
+        organizationId,
+        key: SETTING_KEYS.QUOTE_LAYOUT_CONFIG,
+        value,
+      },
+    });
+
+    revalidatePath("/settings/templates");
+    return validated;
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+}

+ 711 - 0
src/features/settings/Components/InvoiceLayoutEditor.tsx

@@ -0,0 +1,711 @@
+"use client";
+
+import { useState, useCallback, useMemo } from "react";
+import { useTranslations } from "next-intl";
+import {
+  DndContext,
+  closestCenter,
+  KeyboardSensor,
+  PointerSensor,
+  useSensor,
+  useSensors,
+  type DragEndEvent,
+  type UniqueIdentifier,
+} from "@dnd-kit/core";
+import {
+  SortableContext,
+  sortableKeyboardCoordinates,
+  verticalListSortingStrategy,
+  useSortable,
+  arrayMove,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import {
+  GripVertical,
+  Eye,
+  EyeOff,
+  ChevronDown,
+  ChevronRight,
+  Lock,
+  X,
+  Plus,
+  Columns2,
+  Square,
+} from "lucide-react";
+import { Switch } from "@/components/ui/switch";
+import { Button } from "@/components/ui/button";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import {
+  BUILTIN_SECTIONS,
+  SECTIONS_WITH_FIELDS,
+  COLUMN_ELIGIBLE_SECTIONS,
+  getBuiltinFieldsForSection,
+  getBuiltinFieldName,
+  isCustomFieldId,
+  fromCustomFieldId,
+  toCustomFieldId,
+  type InvoiceLayoutConfig,
+  type InvoiceSection,
+  type InvoiceFieldConfig,
+} from "../Schema/invoiceLayoutSchema";
+
+// ---------------------------------------------------------------------------
+// Props
+// ---------------------------------------------------------------------------
+
+interface FieldDef {
+  id: string;
+  name: string;
+  label: string;
+  fieldType: string;
+  entityType: string;
+  options: string | null;
+  required: boolean;
+  sortOrder: number;
+  isActive: boolean;
+}
+
+interface InvoiceLayoutEditorProps {
+  config: InvoiceLayoutConfig;
+  onChange: (config: InvoiceLayoutConfig) => void;
+  documentType: "invoice" | "quote";
+  customFields?: FieldDef[];
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function getSectionName(id: string, t: ReturnType<typeof useTranslations>): string {
+  const key = `layoutEditor.sections.${id}` as Parameters<typeof t>[0];
+  const translated = t(key);
+  // If the key is not found, next-intl returns the key itself
+  if (translated !== key) return translated;
+  const builtin = BUILTIN_SECTIONS.find((s) => s.id === id);
+  return builtin?.name ?? id;
+}
+
+function getFieldDisplayName(
+  fieldId: string,
+  t: ReturnType<typeof useTranslations>,
+  customFields?: FieldDef[],
+): string {
+  if (isCustomFieldId(fieldId)) {
+    const defId = fromCustomFieldId(fieldId);
+    const def = customFields?.find((cf) => cf.id === defId);
+    return def?.label ?? def?.name ?? fieldId;
+  }
+  const key = `layoutEditor.fields.${fieldId}` as Parameters<typeof t>[0];
+  const translated = t(key);
+  if (translated !== key) return translated;
+  return getBuiltinFieldName(fieldId) ?? fieldId;
+}
+
+function getAssignedCustomFieldIds(config: InvoiceLayoutConfig): Set<string> {
+  const assigned = new Set<string>();
+  for (const section of config.sections) {
+    if (section.fields) {
+      for (const field of section.fields) {
+        if (isCustomFieldId(field.id)) {
+          assigned.add(fromCustomFieldId(field.id));
+        }
+      }
+    }
+  }
+  return assigned;
+}
+
+// ---------------------------------------------------------------------------
+// SortableField
+// ---------------------------------------------------------------------------
+
+function SortableField({
+  field,
+  sectionId,
+  isBuiltin,
+  customFields,
+  onToggleVisibility,
+  onRemove,
+  t,
+}: {
+  field: InvoiceFieldConfig;
+  sectionId: string;
+  isBuiltin: boolean;
+  customFields?: FieldDef[];
+  onToggleVisibility: (sectionId: string, fieldId: string) => void;
+  onRemove: (sectionId: string, fieldId: string) => void;
+  t: ReturnType<typeof useTranslations>;
+}) {
+  const {
+    attributes,
+    listeners,
+    setNodeRef,
+    transform,
+    transition,
+    isDragging,
+  } = useSortable({ id: `field::${sectionId}::${field.id}` });
+
+  const style = {
+    transform: CSS.Transform.toString(transform),
+    transition,
+  };
+
+  return (
+    <div
+      ref={setNodeRef}
+      style={style}
+      {...attributes}
+      className={cn(
+        "flex items-center gap-3 rounded-md border border-dashed px-3 py-2 transition-colors",
+        isBuiltin ? "bg-muted/30" : "bg-accent/20 border-accent/40",
+        !field.visible && "opacity-60",
+        isDragging && "z-10 shadow-md",
+      )}
+    >
+      <button
+        type="button"
+        {...listeners}
+        className="shrink-0 cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
+      >
+        <GripVertical className="h-3.5 w-3.5" />
+      </button>
+
+      <span className="flex-1 text-sm text-muted-foreground">
+        {getFieldDisplayName(field.id, t, customFields)}
+        {!isBuiltin && (
+          <span className="ml-1.5 text-xs text-muted-foreground/60">
+            ({t('layoutEditor.custom')})
+          </span>
+        )}
+      </span>
+
+      <div className="flex items-center gap-2">
+        {field.visible ? (
+          <Eye className="h-3 w-3 text-muted-foreground" />
+        ) : (
+          <EyeOff className="h-3 w-3 text-muted-foreground" />
+        )}
+        <Switch
+          size="sm"
+          checked={field.visible}
+          onCheckedChange={() => onToggleVisibility(sectionId, field.id)}
+        />
+        {isBuiltin ? (
+          <Lock className="h-3 w-3 text-muted-foreground/50" />
+        ) : (
+          <button
+            type="button"
+            onClick={() => onRemove(sectionId, field.id)}
+            className="shrink-0 text-muted-foreground hover:text-destructive"
+          >
+            <X className="h-3.5 w-3.5" />
+          </button>
+        )}
+      </div>
+    </div>
+  );
+}
+
+// ---------------------------------------------------------------------------
+// AddCustomFieldButton
+// ---------------------------------------------------------------------------
+
+function AddCustomFieldButton({
+  sectionId,
+  availableFields,
+  onAdd,
+  t,
+}: {
+  sectionId: string;
+  availableFields: FieldDef[];
+  onAdd: (sectionId: string, definitionId: string) => void;
+  t: ReturnType<typeof useTranslations>;
+}) {
+  const [open, setOpen] = useState(false);
+
+  if (availableFields.length === 0) return null;
+
+  return (
+    <Popover open={open} onOpenChange={setOpen}>
+      <PopoverTrigger asChild>
+        <Button
+          type="button"
+          variant="ghost"
+          size="sm"
+          className="h-7 gap-1 text-xs text-muted-foreground"
+        >
+          <Plus className="h-3.5 w-3.5" />
+          {t('layoutEditor.addField')}
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent className="w-56 p-1" align="start">
+        <div className="max-h-48 overflow-y-auto">
+          {availableFields.map((cf) => (
+            <button
+              key={cf.id}
+              type="button"
+              className="flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent"
+              onClick={() => {
+                onAdd(sectionId, cf.id);
+                setOpen(false);
+              }}
+            >
+              {cf.label || cf.name}
+            </button>
+          ))}
+        </div>
+      </PopoverContent>
+    </Popover>
+  );
+}
+
+// ---------------------------------------------------------------------------
+// FieldsPanel – expandable field list inside a section card
+// ---------------------------------------------------------------------------
+
+function FieldsPanel({
+  section,
+  customFields,
+  availableCustomFields,
+  onToggleFieldVisibility,
+  onRemoveField,
+  onAddCustomField,
+  onReorderFields,
+  t,
+}: {
+  section: InvoiceSection;
+  customFields?: FieldDef[];
+  availableCustomFields: FieldDef[];
+  onToggleFieldVisibility: (sectionId: string, fieldId: string) => void;
+  onRemoveField: (sectionId: string, fieldId: string) => void;
+  onAddCustomField: (sectionId: string, definitionId: string) => void;
+  onReorderFields: (sectionId: string, oldIndex: number, newIndex: number) => void;
+  t: ReturnType<typeof useTranslations>;
+}) {
+  const builtinFields = getBuiltinFieldsForSection(section.id);
+  const builtinFieldIds = new Set(builtinFields.map((f) => f.id));
+  const fields = section.fields ?? [];
+
+  const fieldSortableIds: UniqueIdentifier[] = fields.map(
+    (f) => `field::${section.id}::${f.id}`,
+  );
+
+  const fieldSensors = useSensors(
+    useSensor(PointerSensor),
+    useSensor(KeyboardSensor, {
+      coordinateGetter: sortableKeyboardCoordinates,
+    }),
+  );
+
+  const handleFieldDragEnd = (event: DragEndEvent) => {
+    const { active, over } = event;
+    if (!over || active.id === over.id) return;
+    const prefix = `field::${section.id}::`;
+    const activeFieldId = String(active.id).replace(prefix, "");
+    const overFieldId = String(over.id).replace(prefix, "");
+    const oldIndex = fields.findIndex((f) => f.id === activeFieldId);
+    const newIndex = fields.findIndex((f) => f.id === overFieldId);
+    if (oldIndex !== -1 && newIndex !== -1) {
+      onReorderFields(section.id, oldIndex, newIndex);
+    }
+  };
+
+  return (
+    <div className="mt-1 space-y-1">
+      <DndContext
+        sensors={fieldSensors}
+        collisionDetection={closestCenter}
+        onDragEnd={handleFieldDragEnd}
+      >
+        <SortableContext
+          items={fieldSortableIds}
+          strategy={verticalListSortingStrategy}
+        >
+          {fields.map((field) => (
+            <SortableField
+              key={field.id}
+              field={field}
+              sectionId={section.id}
+              isBuiltin={builtinFieldIds.has(field.id)}
+              customFields={customFields}
+              onToggleVisibility={onToggleFieldVisibility}
+              onRemove={onRemoveField}
+              t={t}
+            />
+          ))}
+        </SortableContext>
+      </DndContext>
+      <AddCustomFieldButton
+        sectionId={section.id}
+        availableFields={availableCustomFields}
+        onAdd={onAddCustomField}
+        t={t}
+      />
+    </div>
+  );
+}
+
+// ---------------------------------------------------------------------------
+// Width toggle – simple "full" or "half (L/R)" selector
+// ---------------------------------------------------------------------------
+
+function WidthToggle({
+  column,
+  onChange,
+  t,
+}: {
+  column: "left" | "right" | undefined;
+  onChange: (column: "left" | "right" | undefined) => void;
+  t: ReturnType<typeof useTranslations>;
+}) {
+  // Cycle: undefined (full) → left → right → undefined
+  const isHalf = column === "left" || column === "right";
+
+  return (
+    <button
+      type="button"
+      title={
+        isHalf
+          ? t('layoutEditor.halfWidthHint', { column: column === "left" ? t('layoutEditor.left') : t('layoutEditor.right') })
+          : t('layoutEditor.fullWidthHint')
+      }
+      onClick={() => {
+        if (!column) onChange("left");
+        else if (column === "left") onChange("right");
+        else onChange(undefined);
+      }}
+      className={cn(
+        "flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] font-medium transition-colors",
+        isHalf
+          ? "border-primary/30 bg-primary/10 text-primary"
+          : "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
+      )}
+    >
+      {isHalf ? (
+        <>
+          <Columns2 className="h-3 w-3" />
+          <span className="uppercase">{column === "left" ? t('layoutEditor.L') : t('layoutEditor.R')}</span>
+        </>
+      ) : (
+        <>
+          <Square className="h-3 w-3" />
+          <span>{t('layoutEditor.full')}</span>
+        </>
+      )}
+    </button>
+  );
+}
+
+// ---------------------------------------------------------------------------
+// SectionCard – single section row in the list
+// ---------------------------------------------------------------------------
+
+function SectionCard({
+  section,
+  customFields,
+  onToggleVisibility,
+  onToggleFieldVisibility,
+  onRemoveField,
+  onAddCustomField,
+  onReorderFields,
+  onSetColumn,
+  isExpanded,
+  onToggleExpand,
+  availableCustomFields,
+  t,
+}: {
+  section: InvoiceSection;
+  customFields?: FieldDef[];
+  onToggleVisibility: (sectionId: string) => void;
+  onToggleFieldVisibility: (sectionId: string, fieldId: string) => void;
+  onRemoveField: (sectionId: string, fieldId: string) => void;
+  onAddCustomField: (sectionId: string, definitionId: string) => void;
+  onReorderFields: (sectionId: string, oldIndex: number, newIndex: number) => void;
+  onSetColumn: (sectionId: string, column: "left" | "right" | undefined) => void;
+  isExpanded: boolean;
+  onToggleExpand: (sectionId: string) => void;
+  availableCustomFields: FieldDef[];
+  t: ReturnType<typeof useTranslations>;
+}) {
+  const {
+    attributes,
+    listeners,
+    setNodeRef,
+    transform,
+    transition,
+    isDragging,
+  } = useSortable({ id: section.id });
+
+  const style = {
+    transform: CSS.Transform.toString(transform),
+    transition,
+  };
+
+  const hasFields = SECTIONS_WITH_FIELDS.has(section.id);
+  const isColumnEligible = COLUMN_ELIGIBLE_SECTIONS.has(section.id);
+
+  return (
+    <div
+      ref={setNodeRef}
+      style={style}
+      {...attributes}
+      className={cn(isDragging && "z-10 opacity-50")}
+    >
+      <div
+        className={cn(
+          "flex items-center gap-2 rounded-lg border bg-card px-3 py-2 transition-colors",
+          !section.visible && "opacity-50",
+        )}
+      >
+        {/* Drag handle */}
+        <button
+          type="button"
+          {...listeners}
+          className="shrink-0 cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
+        >
+          <GripVertical className="h-4 w-4" />
+        </button>
+
+        {/* Expand toggle for sections with fields */}
+        {hasFields && (
+          <button
+            type="button"
+            onClick={() => onToggleExpand(section.id)}
+            className="shrink-0 text-muted-foreground hover:text-foreground"
+          >
+            {isExpanded ? (
+              <ChevronDown className="h-4 w-4" />
+            ) : (
+              <ChevronRight className="h-4 w-4" />
+            )}
+          </button>
+        )}
+
+        {/* Section name */}
+        <span className="flex-1 text-sm font-medium">
+          {getSectionName(section.id, t)}
+        </span>
+
+        {/* Width toggle (only for column-eligible sections) */}
+        {isColumnEligible && (
+          <WidthToggle
+            column={section.column}
+            onChange={(col) => onSetColumn(section.id, col)}
+            t={t}
+          />
+        )}
+
+        {/* Visibility toggle */}
+        <div className="flex items-center gap-1.5 ml-1">
+          {section.visible ? (
+            <Eye className="h-3.5 w-3.5 text-muted-foreground" />
+          ) : (
+            <EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
+          )}
+          <Switch
+            size="sm"
+            checked={section.visible}
+            onCheckedChange={() => onToggleVisibility(section.id)}
+          />
+        </div>
+      </div>
+
+      {/* Expandable fields panel */}
+      {hasFields && isExpanded && (
+        <div className="ml-6">
+          <FieldsPanel
+            section={section}
+            customFields={customFields}
+            availableCustomFields={availableCustomFields}
+            onToggleFieldVisibility={onToggleFieldVisibility}
+            onRemoveField={onRemoveField}
+            onAddCustomField={onAddCustomField}
+            onReorderFields={onReorderFields}
+            t={t}
+          />
+        </div>
+      )}
+    </div>
+  );
+}
+
+// ---------------------------------------------------------------------------
+// InvoiceLayoutEditor
+// ---------------------------------------------------------------------------
+
+export function InvoiceLayoutEditor({
+  config,
+  onChange,
+  documentType,
+  customFields,
+}: InvoiceLayoutEditorProps) {
+  const t = useTranslations('settings');
+  const [expandedSections, setExpandedSections] = useState<Set<string>>(
+    new Set(),
+  );
+
+  const sensors = useSensors(
+    useSensor(PointerSensor),
+    useSensor(KeyboardSensor, {
+      coordinateGetter: sortableKeyboardCoordinates,
+    }),
+  );
+
+  const sortedSections = useMemo(
+    () => [...config.sections].sort((a, b) => a.order - b.order),
+    [config.sections],
+  );
+
+  const availableCustomFields = useMemo(() => {
+    if (!customFields) return [];
+    const assigned = getAssignedCustomFieldIds(config);
+    return customFields.filter(
+      (cf) => cf.isActive && !assigned.has(cf.id),
+    );
+  }, [customFields, config]);
+
+  const toggleExpand = useCallback((sectionId: string) => {
+    setExpandedSections((prev) => {
+      const next = new Set(prev);
+      if (next.has(sectionId)) next.delete(sectionId);
+      else next.add(sectionId);
+      return next;
+    });
+  }, []);
+
+  // --- Handlers ---
+
+  const handleToggleVisibility = useCallback(
+    (sectionId: string) => {
+      const updatedSections = config.sections.map((s) =>
+        s.id === sectionId ? { ...s, visible: !s.visible } : s,
+      );
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  const handleToggleFieldVisibility = useCallback(
+    (sectionId: string, fieldId: string) => {
+      const updatedSections = config.sections.map((s) => {
+        if (s.id !== sectionId || !s.fields) return s;
+        return {
+          ...s,
+          fields: s.fields.map((f) =>
+            f.id === fieldId ? { ...f, visible: !f.visible } : f,
+          ),
+        };
+      });
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  const handleRemoveField = useCallback(
+    (sectionId: string, fieldId: string) => {
+      const updatedSections = config.sections.map((s) => {
+        if (s.id !== sectionId || !s.fields) return s;
+        return { ...s, fields: s.fields.filter((f) => f.id !== fieldId) };
+      });
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  const handleAddCustomField = useCallback(
+    (sectionId: string, definitionId: string) => {
+      const cfId = toCustomFieldId(definitionId);
+      const updatedSections = config.sections.map((s) => {
+        if (s.id !== sectionId) return s;
+        const currentFields = s.fields ?? [];
+        return { ...s, fields: [...currentFields, { id: cfId, visible: true }] };
+      });
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  const handleReorderFields = useCallback(
+    (sectionId: string, oldIndex: number, newIndex: number) => {
+      const updatedSections = config.sections.map((s) => {
+        if (s.id !== sectionId || !s.fields) return s;
+        return { ...s, fields: arrayMove(s.fields, oldIndex, newIndex) };
+      });
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  const handleSetColumn = useCallback(
+    (sectionId: string, column: "left" | "right" | undefined) => {
+      const updatedSections = config.sections.map((s) =>
+        s.id === sectionId ? { ...s, column } : s,
+      );
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange],
+  );
+
+  // --- Drag reorder (single flat list) ---
+
+  const handleDragEnd = useCallback(
+    (event: DragEndEvent) => {
+      const { active, over } = event;
+      if (!over || active.id === over.id) return;
+
+      const oldIndex = sortedSections.findIndex((s) => s.id === active.id);
+      const newIndex = sortedSections.findIndex((s) => s.id === over.id);
+      if (oldIndex === -1 || newIndex === -1) return;
+
+      const reordered = arrayMove(sortedSections, oldIndex, newIndex);
+      const updatedSections = config.sections.map((s) => {
+        const idx = reordered.findIndex((r) => r.id === s.id);
+        return { ...s, order: idx };
+      });
+      onChange({ sections: updatedSections });
+    },
+    [config, onChange, sortedSections],
+  );
+
+  return (
+    <div className="space-y-1">
+      <p className="text-xs text-muted-foreground mb-2">
+        {t('layoutEditor.helpText')}
+      </p>
+
+      <DndContext
+        sensors={sensors}
+        collisionDetection={closestCenter}
+        onDragEnd={handleDragEnd}
+      >
+        <SortableContext
+          items={sortedSections.map((s) => s.id)}
+          strategy={verticalListSortingStrategy}
+        >
+          {sortedSections.map((section) => (
+            <SectionCard
+              key={section.id}
+              section={section}
+              customFields={customFields}
+              onToggleVisibility={handleToggleVisibility}
+              onToggleFieldVisibility={handleToggleFieldVisibility}
+              onRemoveField={handleRemoveField}
+              onAddCustomField={handleAddCustomField}
+              onReorderFields={handleReorderFields}
+              onSetColumn={handleSetColumn}
+              isExpanded={expandedSections.has(section.id)}
+              onToggleExpand={toggleExpand}
+              availableCustomFields={availableCustomFields}
+              t={t}
+            />
+          ))}
+        </SortableContext>
+      </DndContext>
+    </div>
+  );
+}

+ 53 - 0
src/features/settings/Components/InvoiceLayoutPreview.tsx

@@ -0,0 +1,53 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+import type { InvoiceLayoutConfig } from '../Schema/invoiceLayoutSchema'
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface FieldDef {
+  id: string
+  name: string
+  label: string
+  fieldType: string
+  entityType: string
+  options: string | null
+  required: boolean
+  sortOrder: number
+  isActive: boolean
+}
+
+interface TemplateValues {
+  primaryColor: string
+  fontFamily: string
+  headerStyle: string
+  logoSize?: number
+}
+
+export interface InvoiceLayoutPreviewProps {
+  config: InvoiceLayoutConfig
+  documentType: 'invoice' | 'quote'
+  customFields?: FieldDef[]
+  template: TemplateValues
+  logoUrl?: string
+}
+
+// The actual renderer must be in a separate file loaded only client-side
+// because @react-pdf/renderer primitives fail during SSR.
+const PreviewRenderer = dynamic(
+  () => import('./InvoiceLayoutPreviewRenderer').then((mod) => mod.InvoiceLayoutPreviewRenderer),
+  {
+    ssr: false,
+    loading: () => (
+      <div className="flex h-[920px] items-center justify-center rounded-lg border bg-gray-100 text-sm text-muted-foreground">
+        Loading preview…
+      </div>
+    ),
+  }
+)
+
+export function InvoiceLayoutPreview(props: InvoiceLayoutPreviewProps) {
+  return <PreviewRenderer {...props} />
+}

+ 276 - 0
src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx

@@ -0,0 +1,276 @@
+"use client";
+
+import { useMemo } from "react";
+import { useMessages } from "next-intl";
+import { PDFViewer } from "@react-pdf/renderer";
+import { InvoicePDF } from "@/features/vehicles/Components/InvoicePDF";
+import { QuotePDF } from "@/features/quotes/Components/QuotePDF";
+import type { InvoiceLayoutPreviewProps } from "./InvoiceLayoutPreview";
+
+// ---------------------------------------------------------------------------
+// Dummy data matching InvoiceData type exactly
+// ---------------------------------------------------------------------------
+
+function buildDummyCustomFields(
+  customFields?: InvoiceLayoutPreviewProps["customFields"],
+) {
+  if (!customFields) return [];
+  return customFields
+    .filter((f) => f.isActive && f.entityType === "service_record")
+    .map((f) => ({
+      fieldId: f.id,
+      label: f.label,
+      value:
+        f.fieldType === "checkbox"
+          ? "true"
+          : f.fieldType === "number"
+            ? "42"
+            : f.fieldType === "date"
+              ? "2026-03-10"
+              : "Sample value",
+      fieldType: f.fieldType,
+    }));
+}
+
+const DUMMY_INVOICE_DATA = {
+  id: "preview-dummy-id-00000001",
+  title: "Brake Service & Oil Change",
+  description: null,
+  type: "Maintenance",
+  serviceDate: new Date("2026-03-10"),
+  shopName: "Your Workshop",
+  techName: "Mike Johnson",
+  mileage: 45230,
+  diagnosticNotes:
+    "<p>Brake wear at 15%. Recommended replacement within 5,000 miles.</p>",
+  invoiceNotes:
+    "<p>Front brake pads replaced. Oil and filter changed with synthetic 5W-30. Next service recommended at 50,000 miles.</p>",
+  subtotal: 314.5,
+  taxRate: 8,
+  taxAmount: 25.16,
+  totalAmount: 339.66,
+  cost: 339.66,
+  invoiceNumber: "INV-2026-1001",
+  discountType: null,
+  discountValue: 0,
+  discountAmount: 0,
+  partItems: [
+    {
+      partNumber: "BP-001",
+      name: "Brake Pads (Front)",
+      quantity: 2,
+      unitPrice: 45.0,
+      total: 90.0,
+    },
+    {
+      partNumber: "OF-042",
+      name: "Oil Filter",
+      quantity: 1,
+      unitPrice: 12.0,
+      total: 12.0,
+    },
+    {
+      partNumber: "SO-530",
+      name: "Synthetic Oil 5W-30",
+      quantity: 5,
+      unitPrice: 8.5,
+      total: 42.5,
+    },
+  ],
+  laborItems: [
+    {
+      description: "Brake Replacement",
+      hours: 1.5,
+      rate: 85.0,
+      total: 127.5,
+    },
+    { description: "Oil Change", hours: 0.5, rate: 85.0, total: 42.5 },
+  ],
+  vehicle: {
+    make: "Toyota",
+    model: "Camry",
+    year: 2022,
+    vin: "1HGBH41JXMN109186",
+    licensePlate: "ABC-1234",
+    mileage: 45230,
+    customer: {
+      name: "John Smith",
+      email: "john@example.com",
+      phone: "(555) 123-4567",
+      address: "123 Main Street, Springfield",
+      company: "Smith Auto Group",
+    },
+  },
+};
+
+const DUMMY_QUOTE_DATA = {
+  quoteNumber: "QT-2026-1001",
+  title: "Brake Service & Oil Change",
+  description: null,
+  validUntil: new Date("2026-04-10"),
+  createdAt: new Date("2026-03-10"),
+  subtotal: 314.5,
+  taxRate: 8,
+  taxAmount: 25.16,
+  discountType: null,
+  discountValue: 0,
+  discountAmount: 0,
+  totalAmount: 339.66,
+  notes: "<p>Front brake pads replacement recommended. Oil and filter change with synthetic 5W-30.</p>",
+  partItems: [
+    {
+      partNumber: "BP-001",
+      name: "Brake Pads (Front)",
+      quantity: 2,
+      unitPrice: 45.0,
+      total: 90.0,
+    },
+    {
+      partNumber: "OF-042",
+      name: "Oil Filter",
+      quantity: 1,
+      unitPrice: 12.0,
+      total: 12.0,
+    },
+    {
+      partNumber: "SO-530",
+      name: "Synthetic Oil 5W-30",
+      quantity: 5,
+      unitPrice: 8.5,
+      total: 42.5,
+    },
+  ],
+  laborItems: [
+    {
+      description: "Brake Replacement",
+      hours: 1.5,
+      rate: 85.0,
+      total: 127.5,
+    },
+    { description: "Oil Change", hours: 0.5, rate: 85.0, total: 42.5 },
+  ],
+  customer: {
+    name: "John Smith",
+    email: "john@example.com",
+    phone: "(555) 123-4567",
+    address: "123 Main Street, Springfield",
+    company: "Smith Auto Group",
+  },
+  vehicle: {
+    make: "Toyota",
+    model: "Camry",
+    year: 2022,
+    vin: "1HGBH41JXMN109186",
+    licensePlate: "ABC-1234",
+  },
+};
+
+const DUMMY_WORKSHOP = {
+  name: "Your Workshop",
+  address: "123 Main Street, Springfield",
+  phone: "(555) 123-4567",
+  email: "shop@example.com",
+};
+
+const DUMMY_INVOICE_SETTINGS = {
+  bankAccount: "1234 5678 9012 3456",
+  orgNumber: "912 345 678",
+  paymentTerms: "Net 14",
+  footerNote: "",
+  showBankAccount: true,
+  showOrgNumber: true,
+  dueDays: 14,
+  currencyCode: "USD",
+  unitSystem: "imperial",
+};
+
+// ---------------------------------------------------------------------------
+// Renderer (loaded only client-side via dynamic import)
+// ---------------------------------------------------------------------------
+
+export function InvoiceLayoutPreviewRenderer({
+  config,
+  documentType,
+  customFields,
+  template,
+  logoUrl,
+}: InvoiceLayoutPreviewProps) {
+  const messages = useMessages();
+
+  const labels = useMemo(() => {
+    const pdf = (messages?.pdf ?? {}) as Record<string, Record<string, string>>;
+    const namespace = documentType === "quote" ? "quote" : "invoice";
+    return {
+      ...(pdf[namespace] ?? {}),
+      ...(pdf.common ?? {}),
+    };
+  }, [messages, documentType]);
+
+  const dummyCf = useMemo(
+    () => buildDummyCustomFields(customFields),
+    [customFields],
+  );
+
+  // Stable key to force PDFViewer remount when layout changes
+  const configKey = useMemo(
+    () =>
+      config.sections
+        .map((s) => `${s.id}:${s.order}:${s.visible}:${s.column ?? "f"}`)
+        .join("|"),
+    [config],
+  );
+
+  const templateConfig = useMemo(
+    () => ({
+      primaryColor: template.primaryColor,
+      fontFamily: template.fontFamily,
+      headerStyle: template.headerStyle,
+      logoSize: template.logoSize,
+      showLogo: true,
+      showCompanyName: true,
+      layoutConfig: config,
+    }),
+    [template, config],
+  );
+
+  const invoiceData = useMemo(
+    () => ({ ...DUMMY_INVOICE_DATA, customFields: dummyCf }),
+    [dummyCf],
+  );
+
+  const isQuote = documentType === "quote";
+
+  return (
+    <div className="rounded-lg shadow-sm overflow-hidden bg-gray-100">
+      <PDFViewer
+        key={configKey}
+        width="100%"
+        height={920}
+        showToolbar={false}
+        style={{ border: "none" }}
+      >
+        {isQuote ? (
+          <QuotePDF
+            data={DUMMY_QUOTE_DATA}
+            workshop={DUMMY_WORKSHOP}
+            currencyCode={DUMMY_INVOICE_SETTINGS.currencyCode}
+            logoDataUri={logoUrl || undefined}
+            template={templateConfig}
+            customFields={dummyCf}
+            labels={labels}
+            layoutConfig={config}
+          />
+        ) : (
+          <InvoicePDF
+            data={invoiceData}
+            workshop={DUMMY_WORKSHOP}
+            invoiceSettings={DUMMY_INVOICE_SETTINGS}
+            logoDataUri={logoUrl || undefined}
+            template={templateConfig}
+            labels={labels}
+          />
+        )}
+      </PDFViewer>
+    </div>
+  );
+}

+ 490 - 0
src/features/settings/Schema/invoiceLayoutSchema.ts

@@ -0,0 +1,490 @@
+import { z } from "zod";
+
+// ---------------------------------------------------------------------------
+// Zod schemas
+// ---------------------------------------------------------------------------
+
+export const invoiceFieldConfigSchema = z.object({
+  id: z.string(),
+  visible: z.boolean(),
+});
+
+export const invoiceSectionSchema = z.object({
+  id: z.string(),
+  visible: z.boolean(),
+  order: z.number().int().nonnegative(),
+  /** When set, the section renders in a 2-column row alongside other column sections. */
+  column: z.enum(["left", "right"]).optional(),
+  /** Controls which fields are shown within this section. */
+  fields: z.array(invoiceFieldConfigSchema).optional(),
+});
+
+export const invoiceLayoutConfigSchema = z.object({
+  sections: z.array(invoiceSectionSchema),
+});
+
+// ---------------------------------------------------------------------------
+// TypeScript types (derived from Zod)
+// ---------------------------------------------------------------------------
+
+export type InvoiceFieldConfig = z.infer<typeof invoiceFieldConfigSchema>;
+export type InvoiceSection = z.infer<typeof invoiceSectionSchema>;
+export type InvoiceLayoutConfig = z.infer<typeof invoiceLayoutConfigSchema>;
+
+// ---------------------------------------------------------------------------
+// Custom field ID helpers
+// ---------------------------------------------------------------------------
+
+export const CUSTOM_FIELD_PREFIX = "cf_";
+
+export function isCustomFieldId(id: string): boolean {
+  return id.startsWith(CUSTOM_FIELD_PREFIX);
+}
+
+export function toCustomFieldId(definitionId: string): string {
+  return `${CUSTOM_FIELD_PREFIX}${definitionId}`;
+}
+
+export function fromCustomFieldId(cfId: string): string {
+  return cfId.slice(CUSTOM_FIELD_PREFIX.length);
+}
+
+// ---------------------------------------------------------------------------
+// Constants – built-in section & field definitions
+// ---------------------------------------------------------------------------
+
+export const BUILTIN_SECTIONS = [
+  { id: "header", name: "Header" },
+  { id: "customer", name: "Customer" },
+  { id: "vehicle", name: "Vehicle" },
+  { id: "service", name: "Service" },
+  { id: "parts_table", name: "Parts Table" },
+  { id: "labor_table", name: "Labor Table" },
+  { id: "totals", name: "Totals" },
+  { id: "notes", name: "Notes" },
+  { id: "diagnostic_notes", name: "Diagnostic Notes" },
+  { id: "bank_account", name: "Bank Account" },
+  { id: "footer", name: "Footer" },
+  { id: "general", name: "General" },
+] as const;
+
+export const BUILTIN_CUSTOMER_FIELDS = [
+  { id: "customer_name", name: "Customer Name" },
+  { id: "customer_company", name: "Customer Company" },
+  { id: "customer_address", name: "Customer Address" },
+  { id: "customer_email", name: "Customer Email" },
+  { id: "customer_phone", name: "Customer Phone" },
+] as const;
+
+export const BUILTIN_VEHICLE_FIELDS = [
+  { id: "vehicle_name", name: "Vehicle" },
+  { id: "vin", name: "VIN" },
+  { id: "license_plate", name: "License Plate" },
+  { id: "mileage", name: "Mileage" },
+] as const;
+
+export const BUILTIN_SERVICE_FIELDS = [
+  { id: "service_title", name: "Service Title" },
+  { id: "service_type", name: "Service Type" },
+  { id: "tech_name", name: "Technician" },
+] as const;
+
+/** @deprecated Use BUILTIN_CUSTOMER_FIELDS, BUILTIN_VEHICLE_FIELDS, BUILTIN_SERVICE_FIELDS */
+export const BUILTIN_INFO_FIELDS = [
+  ...BUILTIN_CUSTOMER_FIELDS,
+  ...BUILTIN_VEHICLE_FIELDS,
+  ...BUILTIN_SERVICE_FIELDS,
+] as const;
+
+export const BUILTIN_HEADER_FIELDS = [
+  { id: "logo", name: "Logo" },
+  { id: "company_name", name: "Company Name" },
+] as const;
+
+export const BUILTIN_BANK_ACCOUNT_FIELDS = [
+  { id: "bank_account", name: "Bank Account" },
+  { id: "org_number", name: "Organization Number" },
+] as const;
+
+export type BuiltinSectionId = (typeof BUILTIN_SECTIONS)[number]["id"];
+export type BuiltinInfoFieldId = (typeof BUILTIN_INFO_FIELDS)[number]["id"];
+export type BuiltinCustomerFieldId = (typeof BUILTIN_CUSTOMER_FIELDS)[number]["id"];
+export type BuiltinVehicleFieldId = (typeof BUILTIN_VEHICLE_FIELDS)[number]["id"];
+export type BuiltinServiceFieldId = (typeof BUILTIN_SERVICE_FIELDS)[number]["id"];
+export type BuiltinHeaderFieldId = (typeof BUILTIN_HEADER_FIELDS)[number]["id"];
+export type BuiltinBankAccountFieldId = (typeof BUILTIN_BANK_ACCOUNT_FIELDS)[number]["id"];
+
+/** Sections that have configurable fields */
+export const SECTIONS_WITH_FIELDS = new Set<string>([
+  "header",
+  "customer",
+  "vehicle",
+  "service",
+  "bank_account",
+  "general",
+]);
+
+/** Sections that can be placed in left/right columns */
+export const COLUMN_ELIGIBLE_SECTIONS = new Set<string>([
+  "customer",
+  "vehicle",
+  "service",
+  "general",
+  "notes",
+  "diagnostic_notes",
+  "bank_account",
+]);
+
+/** Sections that MUST be full-width (cannot be in columns) */
+export const FULL_WIDTH_ONLY_SECTIONS = new Set<string>([
+  "header",
+  "parts_table",
+  "labor_table",
+  "totals",
+  "footer",
+]);
+
+/** Default column assignment for column-eligible sections */
+const DEFAULT_COLUMN: Record<string, "left" | "right"> = {
+  customer: "left",
+  vehicle: "left",
+  service: "right",
+};
+
+// ---------------------------------------------------------------------------
+// Defaults
+// ---------------------------------------------------------------------------
+
+function getDefaultFieldsForSection(sectionId: string): InvoiceFieldConfig[] | undefined {
+  switch (sectionId) {
+    case "customer":
+      return BUILTIN_CUSTOMER_FIELDS.map((f) => ({ id: f.id, visible: true }));
+    case "vehicle":
+      return BUILTIN_VEHICLE_FIELDS.map((f) => ({ id: f.id, visible: true }));
+    case "service":
+      return BUILTIN_SERVICE_FIELDS.map((f) => ({ id: f.id, visible: true }));
+    case "header":
+      return BUILTIN_HEADER_FIELDS.map((f) => ({ id: f.id, visible: true }));
+    case "bank_account":
+      return BUILTIN_BANK_ACCOUNT_FIELDS.map((f) => ({ id: f.id, visible: true }));
+    case "general":
+      return []; // no built-in fields, only custom fields
+    default:
+      return undefined;
+  }
+}
+
+export function getDefaultInvoiceLayout(): InvoiceLayoutConfig {
+  return {
+    sections: BUILTIN_SECTIONS.map((s, index) => {
+      const fields = getDefaultFieldsForSection(s.id);
+      const column = DEFAULT_COLUMN[s.id];
+      return {
+        id: s.id,
+        visible: s.id !== "general", // general hidden by default (no fields)
+        order: index,
+        ...(column ? { column } : {}),
+        ...(fields ? { fields } : {}),
+      };
+    }),
+  };
+}
+
+// ---------------------------------------------------------------------------
+// Field lookup helpers (for rendering)
+// ---------------------------------------------------------------------------
+
+/** Get all built-in field definitions for a section */
+export function getBuiltinFieldsForSection(
+  sectionId: string,
+): ReadonlyArray<{ id: string; name: string }> {
+  switch (sectionId) {
+    case "customer": return BUILTIN_CUSTOMER_FIELDS;
+    case "vehicle": return BUILTIN_VEHICLE_FIELDS;
+    case "service": return BUILTIN_SERVICE_FIELDS;
+    case "header": return BUILTIN_HEADER_FIELDS;
+    case "bank_account": return BUILTIN_BANK_ACCOUNT_FIELDS;
+    default: return [];
+  }
+}
+
+/** Get the display name for a built-in field across all sections */
+export function getBuiltinFieldName(fieldId: string): string | undefined {
+  const allFields = [
+    ...BUILTIN_CUSTOMER_FIELDS,
+    ...BUILTIN_VEHICLE_FIELDS,
+    ...BUILTIN_SERVICE_FIELDS,
+    ...BUILTIN_HEADER_FIELDS,
+    ...BUILTIN_BANK_ACCOUNT_FIELDS,
+  ];
+  return allFields.find((f) => f.id === fieldId)?.name;
+}
+
+// ---------------------------------------------------------------------------
+// Merge helper – fills in missing sections/fields with defaults
+// ---------------------------------------------------------------------------
+
+export function mergeWithDefaults(
+  saved: Partial<InvoiceLayoutConfig>,
+): InvoiceLayoutConfig {
+  const defaults = getDefaultInvoiceLayout();
+
+  if (!saved.sections || saved.sections.length === 0) {
+    return defaults;
+  }
+
+  // Migrate old format: split "info" into customer/vehicle/service
+  const migrated = migrateFromLegacy(saved.sections);
+
+  const merged: InvoiceSection[] = [];
+  const seen = new Set<string>();
+
+  for (const section of migrated) {
+    // Skip duplicate section IDs (keep first occurrence)
+    if (seen.has(section.id)) continue;
+    seen.add(section.id);
+
+    const defaultFields = getDefaultFieldsForSection(section.id);
+    if (defaultFields) {
+      merged.push({
+        ...section,
+        fields: mergeSectionFields(section.fields, defaultFields),
+      });
+    } else {
+      merged.push(section);
+    }
+  }
+
+  // Append any new built-in sections that are missing from saved.
+  let nextOrder = Math.max(...migrated.map((s) => s.order), -1) + 1;
+
+  for (const def of defaults.sections) {
+    if (!seen.has(def.id)) {
+      merged.push({ ...def, order: nextOrder++ });
+    }
+  }
+
+  // Auto-assign column values to column-eligible sections if none have columns
+  const hasAnyColumn = merged.some((s) => s.column);
+  if (!hasAnyColumn) {
+    for (const section of merged) {
+      if (DEFAULT_COLUMN[section.id]) {
+        section.column = DEFAULT_COLUMN[section.id];
+      }
+    }
+  }
+
+  return { sections: merged };
+}
+
+// ---------------------------------------------------------------------------
+// Legacy migration: "info" → customer/vehicle/service,
+//                    "custom_fields" → "general"
+// ---------------------------------------------------------------------------
+
+const CUSTOMER_FIELD_IDS: Set<string> = new Set(BUILTIN_CUSTOMER_FIELDS.map((f) => f.id));
+const VEHICLE_FIELD_IDS: Set<string> = new Set(BUILTIN_VEHICLE_FIELDS.map((f) => f.id));
+const SERVICE_FIELD_IDS: Set<string> = new Set(BUILTIN_SERVICE_FIELDS.map((f) => f.id));
+
+function migrateFromLegacy(sections: InvoiceSection[]): InvoiceSection[] {
+  const hasInfo = sections.some((s) => s.id === "info");
+  const hasCustomFields = sections.some((s) => s.id === "custom_fields");
+
+  // Already in new format
+  if (!hasInfo && !hasCustomFields) {
+    return sections;
+  }
+
+  const result: InvoiceSection[] = [];
+
+  for (const section of sections) {
+    if (section.id === "info") {
+      // Split into customer, vehicle, service
+      const customerFields: InvoiceFieldConfig[] = [];
+      const vehicleFields: InvoiceFieldConfig[] = [];
+      const serviceFields: InvoiceFieldConfig[] = [];
+      const customFieldRefs: InvoiceFieldConfig[] = [];
+
+      if (section.fields) {
+        for (const field of section.fields) {
+          if (CUSTOMER_FIELD_IDS.has(field.id)) {
+            customerFields.push(field);
+          } else if (VEHICLE_FIELD_IDS.has(field.id)) {
+            vehicleFields.push(field);
+          } else if (SERVICE_FIELD_IDS.has(field.id)) {
+            serviceFields.push(field);
+          } else if (isCustomFieldId(field.id)) {
+            customFieldRefs.push(field);
+          }
+        }
+      }
+
+      // Use the info section's order as base, insert three sections
+      const baseOrder = section.order;
+      result.push({
+        id: "customer",
+        visible: section.visible,
+        order: baseOrder,
+        fields: customerFields.length > 0 ? customerFields : undefined,
+      });
+      result.push({
+        id: "vehicle",
+        visible: section.visible,
+        order: baseOrder + 0.1,
+        fields: vehicleFields.length > 0 ? vehicleFields : undefined,
+      });
+      result.push({
+        id: "service",
+        visible: section.visible,
+        order: baseOrder + 0.2,
+        fields: serviceFields.length > 0 ? serviceFields : undefined,
+      });
+
+      // If the old info section had custom fields, add them to general
+      if (customFieldRefs.length > 0) {
+        result.push({
+          id: "general",
+          visible: section.visible,
+          order: baseOrder + 0.3,
+          fields: customFieldRefs,
+        });
+      }
+    } else if (section.id === "custom_fields") {
+      // Rename to general, keep any cf_ field references
+      result.push({
+        ...section,
+        id: "general",
+      });
+    } else {
+      result.push(section);
+    }
+  }
+
+  // Normalize order values to integers
+  result.sort((a, b) => a.order - b.order);
+  result.forEach((s, i) => {
+    s.order = i;
+  });
+
+  return result;
+}
+
+// ---------------------------------------------------------------------------
+// Field merge helper
+// ---------------------------------------------------------------------------
+
+// ---------------------------------------------------------------------------
+// Rendering helper – groups sections into full-width or 2-column rows
+// ---------------------------------------------------------------------------
+
+export type RenderGroup =
+  | { type: "full-width"; sectionId: string }
+  | { type: "columns"; left: string[]; right: string[] };
+
+/**
+ * Groups sorted visible sections for rendering.
+ * Consecutive column-assigned sections are grouped into a single 2-column row.
+ * Sections without a column render as full-width.
+ */
+export function groupSectionsForRendering(
+  sections: InvoiceSection[],
+): RenderGroup[] {
+  // Deduplicate by section ID (keep first occurrence by order)
+  const seen = new Set<string>();
+  const sorted = [...sections]
+    .filter((s) => s.visible)
+    .sort((a, b) => a.order - b.order)
+    .filter((s) => {
+      if (seen.has(s.id)) return false;
+      seen.add(s.id);
+      return true;
+    });
+
+  const groups: RenderGroup[] = [];
+  let pendingLeft: string[] = [];
+  let pendingRight: string[] = [];
+
+  const flushColumns = () => {
+    if (pendingLeft.length > 0 || pendingRight.length > 0) {
+      groups.push({ type: "columns", left: [...pendingLeft], right: [...pendingRight] });
+      pendingLeft = [];
+      pendingRight = [];
+    }
+  };
+
+  for (const section of sorted) {
+    if (section.column === "left" || section.column === "right") {
+      if (section.column === "left") pendingLeft.push(section.id);
+      else pendingRight.push(section.id);
+    } else {
+      flushColumns();
+      groups.push({ type: "full-width", sectionId: section.id });
+    }
+  }
+  flushColumns();
+
+  return groups;
+}
+
+// ---------------------------------------------------------------------------
+// Shared field-ordering helper
+// ---------------------------------------------------------------------------
+
+/**
+ * Returns field IDs in the order specified by a layout config's visible fields Set.
+ * The Set's iteration order reflects the layout config ordering.
+ * Falls back to `defaults` when no config is provided.
+ */
+export function getOrderedFieldIds(
+  visibleFields: Set<string> | null | undefined,
+  defaults: string[],
+): string[] {
+  if (!visibleFields) return defaults;
+  // Set iteration order = insertion order = layout config order
+  const ordered = [...visibleFields].filter((id) => !isCustomFieldId(id));
+  return ordered.length > 0 ? ordered : defaults;
+}
+
+/**
+ * Returns a Set of visible field IDs for a given section, preserving field order.
+ * Returns null if no layout config is present (meaning show all fields).
+ */
+export function getVisibleFieldsForSection(
+  layoutConfig: InvoiceLayoutConfig | undefined | null,
+  sectionId: string,
+): Set<string> | null {
+  if (!layoutConfig) return null;
+  const section = layoutConfig.sections.find((s) => s.id === sectionId);
+  if (!section?.fields) return null;
+  return new Set(section.fields.filter((f) => f.visible).map((f) => f.id));
+}
+
+// ---------------------------------------------------------------------------
+// Field merge helper
+// ---------------------------------------------------------------------------
+
+function mergeSectionFields(
+  savedFields: InvoiceFieldConfig[] | undefined,
+  defaults: InvoiceFieldConfig[],
+): InvoiceFieldConfig[] {
+  if (!savedFields || savedFields.length === 0) {
+    return defaults;
+  }
+
+  const seen = new Set<string>();
+  const merged: InvoiceFieldConfig[] = [];
+
+  for (const field of savedFields) {
+    seen.add(field.id);
+    merged.push(field);
+  }
+
+  // Append any missing default fields
+  for (const def of defaults) {
+    if (!seen.has(def.id)) {
+      merged.push(def);
+    }
+  }
+
+  return merged;
+}

+ 4 - 0
src/features/settings/Schema/settingsSchema.ts

@@ -32,6 +32,8 @@ export const SETTING_KEYS = {
   INVOICE_SHOW_LOGO: "invoice.showLogo",
   INVOICE_SHOW_LOGO: "invoice.showLogo",
   INVOICE_SHOW_COMPANY_NAME: "invoice.showCompanyName",
   INVOICE_SHOW_COMPANY_NAME: "invoice.showCompanyName",
   INVOICE_HEADER_STYLE: "invoice.headerStyle",
   INVOICE_HEADER_STYLE: "invoice.headerStyle",
+  INVOICE_LOGO_SIZE: "invoice.logoSize",
+  QUOTE_LOGO_SIZE: "quote.logoSize",
   PAYMENT_PROVIDERS_ENABLED: "payment.providersEnabled",
   PAYMENT_PROVIDERS_ENABLED: "payment.providersEnabled",
   PAYMENT_STRIPE_SECRET_KEY: "payment.stripe.secretKey",
   PAYMENT_STRIPE_SECRET_KEY: "payment.stripe.secretKey",
   PAYMENT_STRIPE_PUBLISHABLE_KEY: "payment.stripe.publishableKey",
   PAYMENT_STRIPE_PUBLISHABLE_KEY: "payment.stripe.publishableKey",
@@ -73,6 +75,8 @@ export const SETTING_KEYS = {
   WORKBOARD_WEEK_START_DAY: "workboard.weekStartDay",
   WORKBOARD_WEEK_START_DAY: "workboard.weekStartDay",
   WORKBOARD_WORK_DAY_START: "workboard.workDayStart",
   WORKBOARD_WORK_DAY_START: "workboard.workDayStart",
   WORKBOARD_WORK_DAY_END: "workboard.workDayEnd",
   WORKBOARD_WORK_DAY_END: "workboard.workDayEnd",
+  INVOICE_LAYOUT_CONFIG: "invoice.layoutConfig",
+  QUOTE_LAYOUT_CONFIG: "quote.layoutConfig",
 } as const;
 } as const;
 
 
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];

+ 48 - 0
src/features/vehicles/Components/invoice-pdf/CustomFields.tsx

@@ -0,0 +1,48 @@
+import { Text, View } from '@react-pdf/renderer'
+import type { Style } from '@react-pdf/types'
+
+interface CustomFieldsProps {
+  fields: Array<{ fieldId?: string; label: string; value: string; fieldType: string }>
+  styles: Record<string, Style>
+  labels?: Record<string, string>
+}
+
+export function formatFieldValue(value: string, fieldType: string): string {
+  if (fieldType === 'checkbox') {
+    return value === 'true' || value === '1' ? 'Yes' : 'No'
+  }
+
+  if (fieldType === 'date' && value) {
+    const date = new Date(value)
+    if (!isNaN(date.getTime())) {
+      return date.toLocaleDateString('en-US', {
+        year: 'numeric',
+        month: 'long',
+        day: 'numeric',
+      })
+    }
+  }
+
+  return value
+}
+
+export function CustomFields({ fields, styles, labels }: CustomFieldsProps) {
+  const visibleFields = fields.filter((f) => f.value !== '' && f.value != null)
+
+  if (visibleFields.length === 0) {
+    return null
+  }
+
+  const title = labels?.customFieldsTitle || 'Additional Information'
+
+  return (
+    <View style={styles.infoBox}>
+      <Text style={styles.infoLabel}>{title}</Text>
+      {visibleFields.map((field, index) => (
+        <Text key={index} style={styles.infoTextSmall}>
+          {field.label}: {formatFieldValue(field.value, field.fieldType)}
+        </Text>
+      ))}
+    </View>
+  )
+}

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

@@ -46,14 +46,6 @@ export function Footer({
         </View>
         </View>
       )}
       )}
 
 
-      {invoiceSettings?.paymentTerms && (
-        <View style={{ marginTop: 8 }}>
-          <Text style={{ fontSize: 9, color: gray }}>
-            {labels.paymentTerms ? fillTemplate(labels.paymentTerms, { terms: invoiceSettings.paymentTerms }) : `Payment Terms: ${invoiceSettings.paymentTerms}`}
-          </Text>
-        </View>
-      )}
-
       {torqvoiceLogoDataUri ? (
       {torqvoiceLogoDataUri ? (
         <View
         <View
           style={{
           style={{

+ 51 - 49
src/features/vehicles/Components/invoice-pdf/Header.tsx

@@ -16,6 +16,8 @@ interface HeaderProps {
   fontFamily: string
   fontFamily: string
   showLogo: boolean
   showLogo: boolean
   showCompanyName: boolean
   showCompanyName: boolean
+  /** When provided by layoutConfig, takes priority over showLogo/showCompanyName */
+  visibleFields?: Set<string> | null
   logoDataUri?: string
   logoDataUri?: string
   torqvoiceLogoDataUri?: string
   torqvoiceLogoDataUri?: string
   workshop?: WorkshopInfo
   workshop?: WorkshopInfo
@@ -24,6 +26,7 @@ interface HeaderProps {
   invoiceNum: string
   invoiceNum: string
   serviceDate: string
   serviceDate: string
   dueDate: string | null
   dueDate: string | null
+  logoSize?: number
   styles: Record<string, Style>
   styles: Record<string, Style>
   labels: Record<string, string>
   labels: Record<string, string>
 }
 }
@@ -32,8 +35,9 @@ export function Header({
   headerStyle,
   headerStyle,
   primaryColor,
   primaryColor,
   fontFamily,
   fontFamily,
-  showLogo,
-  showCompanyName,
+  showLogo: showLogoProp,
+  showCompanyName: showCompanyNameProp,
+  visibleFields,
   logoDataUri,
   logoDataUri,
   torqvoiceLogoDataUri,
   torqvoiceLogoDataUri,
   workshop,
   workshop,
@@ -42,40 +46,46 @@ export function Header({
   invoiceNum,
   invoiceNum,
   serviceDate,
   serviceDate,
   dueDate,
   dueDate,
+  logoSize,
   styles,
   styles,
   labels,
   labels,
 }: HeaderProps) {
 }: HeaderProps) {
   const fontBold = getFontBold(fontFamily)
   const fontBold = getFontBold(fontFamily)
 
 
+  // Logo size scale factor (default 100 = 1x)
+  const scale = (logoSize || 100) / 100
+
+  // layoutConfig fields take priority over individual toggle props
+  const showLogo = visibleFields ? visibleFields.has('logo') : showLogoProp
+  const showCompanyName = visibleFields ? visibleFields.has('company_name') : showCompanyNameProp
+
   if (headerStyle === 'compact') {
   if (headerStyle === 'compact') {
     return (
     return (
       <View style={{ marginBottom: 20 }}>
       <View style={{ marginBottom: 20 }}>
         <View
         <View
           style={{
           style={{
             flexDirection: 'row',
             flexDirection: 'row',
-            alignItems: 'center',
+            alignItems: 'flex-start',
             justifyContent: 'space-between',
             justifyContent: 'space-between',
             paddingBottom: 10,
             paddingBottom: 10,
             borderBottomWidth: 1,
             borderBottomWidth: 1,
             borderBottomColor: '#e5e7eb',
             borderBottomColor: '#e5e7eb',
           }}
           }}
         >
         >
-          <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+          <View>
             {showLogo && logoDataUri && (
             {showLogo && logoDataUri && (
               <Image
               <Image
                 src={logoDataUri}
                 src={logoDataUri}
-                style={{ maxWidth: 40, maxHeight: 40, borderRadius: 4, objectFit: 'contain' }}
+                style={{ maxWidth: 40 * scale, maxHeight: 40 * scale, borderRadius: 4, objectFit: 'contain', marginBottom: 4 }}
               />
               />
             )}
             )}
             {showCompanyName && (
             {showCompanyName && (
-              <View>
-                <Text style={{ fontSize: 16, fontFamily: fontBold, color: primaryColor }}>
-                  {shopDisplayName}
-                </Text>
-                {workshop?.address && (
-                  <Text style={{ fontSize: 8, color: gray }}>{workshop.address}</Text>
-                )}
-              </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' }}>
           <View style={{ alignItems: 'flex-end' }}>
@@ -126,53 +136,45 @@ export function Header({
             marginHorizontal: -10,
             marginHorizontal: -10,
           }}
           }}
         >
         >
-          <View
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-              justifyContent: 'center',
-              gap: 12,
-            }}
-          >
+          <View style={{ alignItems: 'center' }}>
             {showLogo && logoDataUri && (
             {showLogo && logoDataUri && (
               <Image
               <Image
                 src={logoDataUri}
                 src={logoDataUri}
                 style={{
                 style={{
-                  maxWidth: 50,
-                  maxHeight: 50,
+                  maxWidth: 50 * scale,
+                  maxHeight: 50 * scale,
                   borderRadius: 4,
                   borderRadius: 4,
                   objectFit: 'contain',
                   objectFit: 'contain',
+                  marginBottom: 6,
                 }}
                 }}
               />
               />
             )}
             )}
-            <View style={{ alignItems: 'center' }}>
-              {showCompanyName && (
-                <Text style={{ fontSize: 22, fontFamily: fontBold, color: 'white' }}>
-                  {shopDisplayName}
+            {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)' }}>
+                  {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`}
+                </Text>
+              )}
+              {workshop?.email && (
+                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                  {workshop.email}
                 </Text>
                 </Text>
               )}
               )}
-              {workshop?.address && (
-                <Text style={{ fontSize: 9, color: 'rgba(255,255,255,0.8)', marginTop: 2 }}>
-                  {workshop.address}
+              {invoiceSettings?.showOrgNumber && invoiceSettings?.orgNumber && (
+                <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
+                  {labels.org ? fillTemplate(labels.org, { org: invoiceSettings.orgNumber }) : `Org: ${invoiceSettings.orgNumber}`}
                 </Text>
                 </Text>
               )}
               )}
-              <View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
-                {workshop?.phone && (
-                  <Text style={{ fontSize: 8, color: 'rgba(255,255,255,0.7)' }}>
-                    {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `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)' }}>
-                    {labels.org ? fillTemplate(labels.org, { org: invoiceSettings.orgNumber }) : `Org: ${invoiceSettings.orgNumber}`}
-                  </Text>
-                )}
-              </View>
             </View>
             </View>
           </View>
           </View>
           {torqvoiceLogoDataUri && (
           {torqvoiceLogoDataUri && (
@@ -220,8 +222,8 @@ export function Header({
           <Image
           <Image
             src={logoDataUri}
             src={logoDataUri}
             style={{
             style={{
-              maxWidth: 150,
-              maxHeight: 60,
+              maxWidth: 150 * scale,
+              maxHeight: 60 * scale,
               marginBottom: 6,
               marginBottom: 6,
               borderRadius: 4,
               borderRadius: 4,
               objectFit: 'contain',
               objectFit: 'contain',

+ 258 - 40
src/features/vehicles/Components/invoice-pdf/InfoSection.tsx

@@ -1,6 +1,9 @@
+import React from 'react'
 import { Text, View } from '@react-pdf/renderer'
 import { Text, View } from '@react-pdf/renderer'
 import type { InvoiceData, InvoiceSettingsProps } from './types'
 import type { InvoiceData, InvoiceSettingsProps } from './types'
 import type { Style } from '@react-pdf/types'
 import type { Style } from '@react-pdf/types'
+import { isCustomFieldId, getOrderedFieldIds } from '@/features/settings/Schema/invoiceLayoutSchema'
+import { formatFieldValue } from './CustomFields'
 
 
 function fillTemplate(template: string, values: Record<string, string>): string {
 function fillTemplate(template: string, values: Record<string, string>): string {
   return Object.entries(values).reduce(
   return Object.entries(values).reduce(
@@ -9,55 +12,270 @@ function fillTemplate(template: string, values: Record<string, string>): string
   )
   )
 }
 }
 
 
-interface InfoSectionProps {
+interface CustomFieldEntry {
+  fieldId: string
+  label: string
+  value: string
+  fieldType: string
+}
+
+/**
+ * Renders custom fields inline, ordered by visibleFields if available.
+ */
+function renderCustomFields(
+  customFields: CustomFieldEntry[] | undefined,
+  visibleFields: Set<string> | null | undefined,
+  styles: Record<string, Style>,
+) {
+  if (!customFields || customFields.length === 0) return null
+
+  let ordered = customFields
+  if (visibleFields) {
+    const cfOrder = [...visibleFields].filter(id => isCustomFieldId(id))
+    const cfMap = new Map(customFields.map(cf => [`cf_${cf.fieldId}`, cf]))
+    ordered = cfOrder.map(id => cfMap.get(id)).filter(Boolean) as CustomFieldEntry[]
+  }
+
+  return (
+    <>
+      {ordered
+        .filter(cf => cf.value !== '' && cf.value != null)
+        .map((cf, i) => (
+          <Text key={`cf-${i}`} style={styles.infoTextSmall}>
+            {cf.label}: {formatFieldValue(cf.value, cf.fieldType)}
+          </Text>
+        ))}
+    </>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// Field renderers keyed by field ID
+// ---------------------------------------------------------------------------
+
+interface CustomerRenderCtx {
+  data: InvoiceData
+  styles: Record<string, Style>
+  labels: Record<string, string>
+}
+
+function renderCustomerField(fieldId: string, ctx: CustomerRenderCtx): React.ReactNode {
+  const { data, styles, labels } = ctx
+  const c = data.vehicle.customer
+  if (!c) return null
+
+  switch (fieldId) {
+    case 'customer_name':
+      return <Text key={fieldId} style={styles.infoTextBold}>{c.name}</Text>
+    case 'customer_company':
+      return c.company ? <Text key={fieldId} style={styles.infoText}>{c.company}</Text> : null
+    case 'customer_address':
+      return c.address ? <Text key={fieldId} style={styles.infoTextSmall}>{c.address}</Text> : null
+    case 'customer_email':
+      return c.email ? <Text key={fieldId} style={styles.infoTextSmall}>{c.email}</Text> : null
+    case 'customer_phone':
+      return c.phone ? <Text key={fieldId} style={styles.infoTextSmall}>{c.phone}</Text> : null
+    default:
+      return null
+  }
+}
+
+interface VehicleRenderCtx {
   data: InvoiceData
   data: InvoiceData
-  vehicleName: string
+  vehicleName?: string
   invoiceSettings?: InvoiceSettingsProps
   invoiceSettings?: InvoiceSettingsProps
   styles: Record<string, Style>
   styles: Record<string, Style>
   labels: Record<string, string>
   labels: Record<string, string>
 }
 }
 
 
-export function InfoSection({ data, vehicleName, invoiceSettings, styles, labels }: InfoSectionProps) {
+function renderVehicleField(fieldId: string, ctx: VehicleRenderCtx): React.ReactNode {
+  const { data, vehicleName, invoiceSettings, styles, labels } = ctx
+
+  switch (fieldId) {
+    case 'vehicle_name':
+      return <Text key={fieldId} style={styles.infoTextBold}>{vehicleName}</Text>
+    case 'vin':
+      return data.vehicle.vin ? (
+        <Text key={fieldId} style={styles.infoTextSmall}>
+          {labels.vin ? fillTemplate(labels.vin, { vin: data.vehicle.vin }) : `VIN: ${data.vehicle.vin}`}
+        </Text>
+      ) : null
+    case 'license_plate':
+      return data.vehicle.licensePlate ? (
+        <Text key={fieldId} style={styles.infoTextSmall}>
+          {labels.plate ? fillTemplate(labels.plate, { plate: data.vehicle.licensePlate }) : `Plate: ${data.vehicle.licensePlate}`}
+        </Text>
+      ) : null
+    case 'mileage':
+      return data.mileage ? (
+        <Text key={fieldId} style={styles.infoTextSmall}>
+          {labels.mileage ? fillTemplate(labels.mileage, { mileage: data.mileage.toLocaleString() }) : `Mileage: ${data.mileage.toLocaleString()}`}{' '}
+          {invoiceSettings?.unitSystem === 'metric' ? (labels.km || 'km') : (labels.mi || 'mi')}
+        </Text>
+      ) : null
+    default:
+      return null
+  }
+}
+
+interface ServiceRenderCtx {
+  data: InvoiceData
+  styles: Record<string, Style>
+  labels: Record<string, string>
+}
+
+function renderServiceField(fieldId: string, ctx: ServiceRenderCtx): React.ReactNode {
+  const { data, styles, labels } = ctx
+
+  switch (fieldId) {
+    case 'service_title':
+      return <Text key={fieldId} style={styles.infoTextBold}>{data.title}</Text>
+    case 'service_type':
+      return (
+        <Text key={fieldId} style={styles.infoTextSmall}>
+          {labels.type ? fillTemplate(labels.type, { type: data.type }) : `Type: ${data.type}`}
+        </Text>
+      )
+    case 'tech_name':
+      return data.techName ? (
+        <Text key={fieldId} style={styles.infoTextSmall}>
+          {labels.tech ? fillTemplate(labels.tech, { tech: data.techName }) : `Tech: ${data.techName}`}
+        </Text>
+      ) : null
+    default:
+      return null
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Default field orders (used when no layout config / visibleFields)
+// ---------------------------------------------------------------------------
+
+const DEFAULT_CUSTOMER_FIELDS = ['customer_name', 'customer_company', 'customer_address', 'customer_email', 'customer_phone']
+const DEFAULT_VEHICLE_FIELDS = ['vehicle_name', 'vin', 'license_plate', 'mileage']
+const DEFAULT_SERVICE_FIELDS = ['service_title', 'service_type', 'tech_name']
+
+// ---------------------------------------------------------------------------
+// Section components
+// ---------------------------------------------------------------------------
+
+/**
+ * Customer info section (Bill To).
+ */
+export function CustomerSection({
+  data,
+  styles,
+  labels,
+  visibleFields,
+  customFields,
+}: {
+  data: InvoiceData
+  styles: Record<string, Style>
+  labels: Record<string, string>
+  visibleFields?: Set<string> | null
+  customFields?: CustomFieldEntry[]
+}) {
+  const show = (id: string) => !visibleFields || visibleFields.has(id)
+  const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_CUSTOMER_FIELDS)
+
+  const hasCustomer = data.vehicle.customer &&
+    (show('customer_name') ||
+      (show('customer_company') && data.vehicle.customer.company) ||
+      (show('customer_address') && data.vehicle.customer.address) ||
+      (show('customer_email') && data.vehicle.customer.email) ||
+      (show('customer_phone') && data.vehicle.customer.phone))
+
+  const hasCf = customFields?.some(cf => cf.value !== '' && cf.value != null)
+
+  if (!hasCustomer && !hasCf) return null
+
+  const ctx: CustomerRenderCtx = { data, styles, labels }
+
   return (
   return (
-    <View style={styles.infoRow}>
+    <View style={styles.infoBox}>
+      <Text style={styles.infoLabel}>{labels.billTo || 'Bill To'}</Text>
       {data.vehicle.customer && (
       {data.vehicle.customer && (
-        <View style={styles.infoBox}>
-          <Text style={styles.infoLabel}>{labels.billTo || '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>
+        <>
+          {fieldOrder.filter(id => show(id)).map(id => renderCustomerField(id, ctx))}
+        </>
       )}
       )}
-      <View style={styles.infoBox}>
-        <Text style={styles.infoLabel}>{labels.vehicle || 'Vehicle'}</Text>
-        <Text style={styles.infoTextBold}>{vehicleName}</Text>
-        {data.vehicle.vin && <Text style={styles.infoTextSmall}>{labels.vin ? fillTemplate(labels.vin, { vin: data.vehicle.vin }) : `VIN: ${data.vehicle.vin}`}</Text>}
-        {data.vehicle.licensePlate && (
-          <Text style={styles.infoTextSmall}>{labels.plate ? fillTemplate(labels.plate, { plate: data.vehicle.licensePlate }) : `Plate: ${data.vehicle.licensePlate}`}</Text>
-        )}
-        {data.mileage && (
-          <Text style={styles.infoTextSmall}>
-            {labels.mileage ? fillTemplate(labels.mileage, { mileage: data.mileage.toLocaleString() }) : `Mileage: ${data.mileage.toLocaleString()}`}{' '}
-            {invoiceSettings?.unitSystem === 'metric' ? (labels.km || 'km') : (labels.mi || 'mi')}
-          </Text>
-        )}
-      </View>
-      <View style={styles.infoBox}>
-        <Text style={styles.infoLabel}>{labels.service || 'Service'}</Text>
-        <Text style={styles.infoTextBold}>{data.title}</Text>
-        <Text style={styles.infoTextSmall}>{labels.type ? fillTemplate(labels.type, { type: data.type }) : `Type: ${data.type}`}</Text>
-        {data.techName && <Text style={styles.infoTextSmall}>{labels.tech ? fillTemplate(labels.tech, { tech: data.techName }) : `Tech: ${data.techName}`}</Text>}
-      </View>
+      {renderCustomFields(customFields, visibleFields, styles)}
+    </View>
+  )
+}
+
+/**
+ * Vehicle info section.
+ */
+export function VehicleSection({
+  data,
+  vehicleName,
+  invoiceSettings,
+  styles,
+  labels,
+  visibleFields,
+  customFields,
+}: {
+  data: InvoiceData
+  vehicleName?: string
+  invoiceSettings?: InvoiceSettingsProps
+  styles: Record<string, Style>
+  labels: Record<string, string>
+  visibleFields?: Set<string> | null
+  customFields?: CustomFieldEntry[]
+}) {
+  const show = (id: string) => !visibleFields || visibleFields.has(id)
+  const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_VEHICLE_FIELDS)
+
+  const hasVehicle =
+    show('vehicle_name') ||
+    (show('vin') && data.vehicle.vin) ||
+    (show('license_plate') && data.vehicle.licensePlate) ||
+    (show('mileage') && data.mileage)
+
+  const hasCf = customFields?.some(cf => cf.value !== '' && cf.value != null)
+
+  if (!hasVehicle && !hasCf) return null
+
+  const ctx: VehicleRenderCtx = { data, vehicleName, invoiceSettings, styles, labels }
+
+  return (
+    <View style={styles.infoBox}>
+      <Text style={styles.infoLabel}>{labels.vehicle || 'Vehicle'}</Text>
+      {fieldOrder.filter(id => show(id)).map(id => renderVehicleField(id, ctx))}
+      {renderCustomFields(customFields, visibleFields, styles)}
+    </View>
+  )
+}
+
+export function ServiceSection({ data, styles, labels, visibleFields, customFields }: {
+  data: InvoiceData
+  vehicleName?: string
+  invoiceSettings?: InvoiceSettingsProps
+  styles: Record<string, Style>
+  labels: Record<string, string>
+  visibleFields?: Set<string> | null
+  customFields?: CustomFieldEntry[]
+}) {
+  const show = (fieldId: string) => !visibleFields || visibleFields.has(fieldId)
+  const fieldOrder = getOrderedFieldIds(visibleFields, DEFAULT_SERVICE_FIELDS)
+
+  const hasBuiltinContent =
+    show('service_title') ||
+    show('service_type') ||
+    (show('tech_name') && data.techName)
+
+  const hasCfContent = customFields && customFields.some(cf => cf.value !== '' && cf.value != null)
+
+  if (!hasBuiltinContent && !hasCfContent) return null
+
+  const ctx: ServiceRenderCtx = { data, styles, labels }
+
+  return (
+    <View style={styles.infoBox}>
+      <Text style={styles.infoLabel}>{labels.service || 'Service'}</Text>
+      {fieldOrder.filter(id => show(id)).map(id => renderServiceField(id, ctx))}
+      {renderCustomFields(customFields, visibleFields, styles)}
     </View>
     </View>
   )
   )
 }
 }

+ 190 - 54
src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx

@@ -1,12 +1,16 @@
+import React from 'react'
 import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
 import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
 import { formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { createStyles, gray, getFontBold } from './styles'
 import { createStyles, gray, getFontBold } from './styles'
 import { Header } from './Header'
 import { Header } from './Header'
-import { InfoSection } from './InfoSection'
+import { CustomerSection, VehicleSection, ServiceSection } from './InfoSection'
 import { PartsTable, LaborTable } from './Tables'
 import { PartsTable, LaborTable } from './Tables'
 import { Totals } from './Totals'
 import { Totals } from './Totals'
-import { Notes } from './Notes'
+import { NotesOnly, BankAccountSection, DiagnosticNotesSection } from './Notes'
+import { CustomFields } from './CustomFields'
 import { Footer, AttachmentsFooter } from './Footer'
 import { Footer, AttachmentsFooter } from './Footer'
+import type { InvoiceLayoutConfig } from '@/features/settings/Schema/invoiceLayoutSchema'
+import { isCustomFieldId, fromCustomFieldId, groupSectionsForRendering, getDefaultInvoiceLayout, getVisibleFieldsForSection } from '@/features/settings/Schema/invoiceLayoutSchema'
 import type {
 import type {
   TemplateConfig,
   TemplateConfig,
   InvoiceData,
   InvoiceData,
@@ -17,6 +21,26 @@ import type {
   OtherAttachment,
   OtherAttachment,
 } from './types'
 } from './types'
 
 
+// ---------------------------------------------------------------------------
+// Layout config helpers
+// ---------------------------------------------------------------------------
+
+function getCustomFieldsForSection(
+  layoutConfig: InvoiceLayoutConfig | undefined,
+  sectionId: string,
+  allCustomFields: Array<{ fieldId: string; label: string; value: string; fieldType: string }>,
+): Array<{ fieldId: string; label: string; value: string; fieldType: string }> {
+  if (!layoutConfig || !allCustomFields?.length) return []
+  const section = layoutConfig.sections.find(s => s.id === sectionId)
+  if (!section?.fields) return []
+  const cfIds = new Set(
+    section.fields
+      .filter(f => f.visible && isCustomFieldId(f.id))
+      .map(f => fromCustomFieldId(f.id))
+  )
+  return allCustomFields.filter(cf => cfIds.has(cf.fieldId))
+}
+
 export function InvoicePDF({
 export function InvoicePDF({
   data,
   data,
   workshop,
   workshop,
@@ -78,38 +102,91 @@ export function InvoicePDF({
   const shopDisplayName = workshop?.name || data.shopName || 'Torqvoice'
   const shopDisplayName = workshop?.name || data.shopName || 'Torqvoice'
   const hasAttachments = imageAttachments.length > 0 || otherAttachments.length > 0
   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}
-          labels={labels}
-        />
+  // ---------------------------------------------------------------------------
+  // Layout-driven section rendering
+  // ---------------------------------------------------------------------------
+  const layoutConfig = template?.layoutConfig
+  const visibleCustomerFields = getVisibleFieldsForSection(layoutConfig, 'customer')
+  const visibleVehicleFields = getVisibleFieldsForSection(layoutConfig, 'vehicle')
+  const visibleServiceFields = getVisibleFieldsForSection(layoutConfig, 'service')
+  const visibleHeaderFields = getVisibleFieldsForSection(layoutConfig, 'header')
+  const visibleBankAccountFields = getVisibleFieldsForSection(layoutConfig, 'bank_account')
 
 
-        <InfoSection
-          data={data}
-          vehicleName={vehicleName}
-          invoiceSettings={invoiceSettings}
-          styles={styles}
-          labels={labels}
-        />
+  const allCf = data.customFields || []
+  const customerCf = getCustomFieldsForSection(layoutConfig, 'customer', allCf)
+  const vehicleCf = getCustomFieldsForSection(layoutConfig, 'vehicle', allCf)
+  const serviceCf = getCustomFieldsForSection(layoutConfig, 'service', allCf)
+  const generalCf = getCustomFieldsForSection(layoutConfig, 'general', allCf)
+  // If no layout config, all custom fields go to the general section
+  const generalFallbackCf = !layoutConfig ? allCf : generalCf
+
+  // Map each section ID to its JSX. Sections that have no data naturally
+  // return null and will be skipped by React.
+  const sectionMap: Record<string, React.ReactNode> = {
+    header: (
+      <Header
+        headerStyle={headerStyle}
+        primaryColor={primaryColor}
+        fontFamily={fontFamily}
+        showLogo={showLogo}
+        showCompanyName={showCompanyName}
+        visibleFields={visibleHeaderFields}
+        logoDataUri={logoDataUri}
+        torqvoiceLogoDataUri={torqvoiceLogoDataUri}
+        workshop={workshop}
+        invoiceSettings={invoiceSettings}
+        shopDisplayName={shopDisplayName}
+        invoiceNum={invoiceNum}
+        serviceDate={serviceDate}
+        dueDate={dueDate}
+        logoSize={template?.logoSize}
+        styles={styles}
+        labels={labels}
+      />
+    ),
+
+    customer: (
+      <CustomerSection
+        data={data}
+        styles={styles}
+        labels={labels}
+        visibleFields={visibleCustomerFields}
+        customFields={customerCf}
+      />
+    ),
+
+    vehicle: (
+      <VehicleSection
+        data={data}
+        vehicleName={vehicleName}
+        invoiceSettings={invoiceSettings}
+        styles={styles}
+        labels={labels}
+        visibleFields={visibleVehicleFields}
+        customFields={vehicleCf}
+      />
+    ),
+
+    service: (
+      <ServiceSection
+        data={data}
+        styles={styles}
+        labels={labels}
+        visibleFields={visibleServiceFields}
+        customFields={serviceCf}
+      />
+    ),
+
+    parts_table: (
+      <PartsTable data={data} currencyCode={cc} styles={styles} labels={labels} />
+    ),
 
 
-        <PartsTable data={data} currencyCode={cc} styles={styles} labels={labels} />
-        <LaborTable data={data} currencyCode={cc} styles={styles} labels={labels} />
+    labor_table: (
+      <LaborTable data={data} currencyCode={cc} styles={styles} labels={labels} />
+    ),
 
 
+    totals: (
+      <>
         <Totals
         <Totals
           data={data}
           data={data}
           currencyCode={cc}
           currencyCode={cc}
@@ -124,7 +201,6 @@ export function InvoicePDF({
           styles={styles}
           styles={styles}
           labels={labels}
           labels={labels}
         />
         />
-
         {torqvoiceLogoDataUri && (
         {torqvoiceLogoDataUri && (
           <View
           <View
             style={{
             style={{
@@ -140,30 +216,90 @@ export function InvoicePDF({
             <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
             <Text style={{ fontSize: 7, color: gray, fontFamily: fontBold }}>Torqvoice</Text>
           </View>
           </View>
         )}
         )}
+      </>
+    ),
 
 
-        <Notes
-          invoiceNotes={data.invoiceNotes}
-          diagnosticNotes={data.diagnosticNotes}
-          invoiceSettings={invoiceSettings}
-          otherAttachments={otherAttachments}
-          pdfAttachmentNames={pdfAttachmentNames}
-          fontFamily={fontFamily}
-          styles={styles}
-          labels={labels}
-        />
+    general:
+      generalFallbackCf.length > 0 ? (
+        <CustomFields fields={generalFallbackCf} styles={styles} labels={labels} />
+      ) : null,
 
 
-        <Footer
-          shopDisplayName={shopDisplayName}
-          serviceDate={serviceDate}
-          invoiceSettings={invoiceSettings}
-          invoiceNum={invoiceNum}
-          primaryColor={primaryColor}
-          fontFamily={fontFamily}
-          torqvoiceLogoDataUri={torqvoiceLogoDataUri}
-          portalUrl={portalUrl}
-          styles={styles}
-          labels={labels}
-        />
+    notes: (
+      <NotesOnly
+        invoiceNotes={data.invoiceNotes}
+        otherAttachments={otherAttachments}
+        pdfAttachmentNames={pdfAttachmentNames}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+    ),
+
+    diagnostic_notes: (
+      <DiagnosticNotesSection
+        diagnosticNotes={data.diagnosticNotes}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+    ),
+
+    bank_account: (
+      <BankAccountSection
+        invoiceSettings={invoiceSettings}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+        visibleFields={visibleBankAccountFields}
+        primaryColor={primaryColor}
+        dueDate={dueDate}
+      />
+    ),
+
+    footer: (
+      <Footer
+        shopDisplayName={shopDisplayName}
+        serviceDate={serviceDate}
+        invoiceSettings={invoiceSettings}
+        invoiceNum={invoiceNum}
+        primaryColor={primaryColor}
+        fontFamily={fontFamily}
+        torqvoiceLogoDataUri={torqvoiceLogoDataUri}
+        portalUrl={portalUrl}
+        styles={styles}
+        labels={labels}
+      />
+    ),
+  }
+
+  // Use column-based grouping from layout config.
+  const effectiveSections = layoutConfig?.sections ?? getDefaultInvoiceLayout().sections
+  const renderGroups = groupSectionsForRendering(effectiveSections)
+  const renderedSections: React.ReactNode[] = []
+
+  for (const group of renderGroups) {
+    if (group.type === 'full-width') {
+      renderedSections.push(
+        <React.Fragment key={group.sectionId}>{sectionMap[group.sectionId]}</React.Fragment>
+      )
+    } else {
+      const leftNodes = group.left.map(id => <React.Fragment key={id}>{sectionMap[id]}</React.Fragment>).filter(Boolean)
+      const rightNodes = group.right.map(id => <React.Fragment key={id}>{sectionMap[id]}</React.Fragment>).filter(Boolean)
+      if (leftNodes.length > 0 || rightNodes.length > 0) {
+        renderedSections.push(
+          <View key={`col-${group.left[0] || group.right[0]}`} style={styles.infoRow}>
+            <View style={{ flex: 1, gap: 4 }}>{leftNodes}</View>
+            <View style={{ flex: 1, gap: 4 }}>{rightNodes}</View>
+          </View>
+        )
+      }
+    }
+  }
+
+  return (
+    <Document>
+      <Page size="A4" style={styles.page}>
+        {renderedSections}
       </Page>
       </Page>
 
 
       {hasAttachments && imageAttachments.length > 0 && (
       {hasAttachments && imageAttachments.length > 0 && (

+ 182 - 23
src/features/vehicles/Components/invoice-pdf/Notes.tsx

@@ -174,45 +174,39 @@ function hasContent(html: string | null): boolean {
   return html.replace(/<[^>]*>/g, '').trim().length > 0
   return html.replace(/<[^>]*>/g, '').trim().length > 0
 }
 }
 
 
-export function Notes({
+// ---------------------------------------------------------------------------
+// Individual section components – used by InvoicePDF when layout ordering is
+// active so that notes, bank account, and diagnostic notes can be rendered
+// independently and in any order.
+// ---------------------------------------------------------------------------
+
+export function NotesOnly({
   invoiceNotes,
   invoiceNotes,
-  diagnosticNotes,
-  invoiceSettings,
   otherAttachments,
   otherAttachments,
   pdfAttachmentNames,
   pdfAttachmentNames,
   fontFamily,
   fontFamily,
   styles,
   styles,
   labels,
   labels,
-}: NotesProps) {
+}: {
+  invoiceNotes: string | null
+  otherAttachments: OtherAttachment[]
+  pdfAttachmentNames: string[]
+  fontFamily: string
+  styles: Record<string, Style>
+  labels: Record<string, string>
+}) {
   const fontBold = getFontBold(fontFamily)
   const fontBold = getFontBold(fontFamily)
-
   return (
   return (
     <>
     <>
       {hasContent(invoiceNotes) && (
       {hasContent(invoiceNotes) && (
-        <View style={styles.notesSection}>
+        <View wrap={false} style={styles.notesSection}>
           <Text style={styles.notesLabel}>{labels.notes || 'Notes'}</Text>
           <Text style={styles.notesLabel}>{labels.notes || 'Notes'}</Text>
           <HtmlToPdf html={invoiceNotes!} baseStyle={styles.notesText} fontBold={fontBold} />
           <HtmlToPdf html={invoiceNotes!} baseStyle={styles.notesText} fontBold={fontBold} />
         </View>
         </View>
       )}
       )}
 
 
-      {invoiceSettings?.showBankAccount && invoiceSettings?.bankAccount && (
-        <View style={{ ...styles.notesSection, marginTop: 12 }}>
-          <Text style={styles.notesLabel}>{labels.bankAccount || 'Til Konto / Bank Account'}</Text>
-          <Text style={{ fontSize: 11, fontFamily: fontBold }}>
-            {invoiceSettings.bankAccount}
-          </Text>
-        </View>
-      )}
-
-      {hasContent(diagnosticNotes) && (
-        <View style={{ ...styles.notesSection, marginTop: 8 }}>
-          <Text style={styles.notesLabel}>{labels.diagnosticNotes || 'Diagnostic Notes'}</Text>
-          <HtmlToPdf html={diagnosticNotes!} baseStyle={styles.notesText} fontBold={fontBold} />
-        </View>
-      )}
-
       {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
       {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && (
-        <View style={{ ...styles.notesSection, marginTop: 8 }}>
+        <View wrap={false} style={{ ...styles.notesSection, marginTop: 8 }}>
           <Text style={styles.notesLabel}>{labels.attachedDocuments || 'Attached Documents'}</Text>
           <Text style={styles.notesLabel}>{labels.attachedDocuments || 'Attached Documents'}</Text>
           {pdfAttachmentNames.map((name, i) => (
           {pdfAttachmentNames.map((name, i) => (
             <Text key={`pdf-${i}`} style={styles.notesText}>
             <Text key={`pdf-${i}`} style={styles.notesText}>
@@ -229,3 +223,168 @@ export function Notes({
     </>
     </>
   )
   )
 }
 }
+
+export function BankAccountSection({
+  invoiceSettings,
+  fontFamily,
+  styles,
+  labels,
+  visibleFields,
+  primaryColor = '#d97706',
+  dueDate,
+}: {
+  invoiceSettings?: InvoiceSettingsProps
+  fontFamily: string
+  styles: Record<string, Style>
+  labels: Record<string, string>
+  /** When provided by layoutConfig, takes priority over individual toggle props */
+  visibleFields?: Set<string> | null
+  primaryColor?: string
+  dueDate?: string | null
+}) {
+  const fontBold = getFontBold(fontFamily)
+
+  // layoutConfig fields take priority over individual toggle props
+  const showBankAccount = visibleFields
+    ? visibleFields.has('bank_account')
+    : (invoiceSettings?.showBankAccount ?? false)
+  const showOrgNumber = visibleFields
+    ? visibleFields.has('org_number')
+    : (invoiceSettings?.showOrgNumber ?? false)
+
+  const hasBankAccount = showBankAccount && invoiceSettings?.bankAccount
+  const hasOrgNumber = showOrgNumber && invoiceSettings?.orgNumber
+  const hasPaymentTerms = !!invoiceSettings?.paymentTerms
+  const hasDueDate = !!dueDate
+
+  if (!hasBankAccount && !hasOrgNumber && !hasPaymentTerms && !hasDueDate) return null
+
+  // Convert hex color to rgba-like background (10% opacity)
+  const bgColor = `${primaryColor}14`
+  const borderColor = primaryColor
+
+  return (
+    <View wrap={false} style={{
+      marginTop: 12,
+      padding: 12,
+      borderWidth: 1,
+      borderColor: borderColor,
+      borderRadius: 4,
+      backgroundColor: bgColor,
+    }}>
+      <Text style={{
+        fontSize: 9,
+        fontFamily: fontBold,
+        color: primaryColor,
+        textTransform: 'uppercase',
+        letterSpacing: 0.5,
+        marginBottom: 8,
+      }}>
+        {labels.paymentInformation || 'Payment Information'}
+      </Text>
+      <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
+        {hasBankAccount && (
+          <View style={{ minWidth: '40%' }}>
+            <Text style={{ fontSize: 8, color: '#6b7280', marginBottom: 2 }}>
+              {labels.bankAccount || 'Bank Account'}
+            </Text>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>
+              {invoiceSettings!.bankAccount}
+            </Text>
+          </View>
+        )}
+        {hasOrgNumber && (
+          <View style={{ minWidth: '40%' }}>
+            <Text style={{ fontSize: 8, color: '#6b7280', marginBottom: 2 }}>
+              {labels.orgNumberLabel || 'Org. Number'}
+            </Text>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>
+              {invoiceSettings!.orgNumber}
+            </Text>
+          </View>
+        )}
+        {hasPaymentTerms && (
+          <View style={{ minWidth: '40%' }}>
+            <Text style={{ fontSize: 8, color: '#6b7280', marginBottom: 2 }}>
+              {labels.paymentTermsLabel || 'Payment Terms'}
+            </Text>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>
+              {invoiceSettings!.paymentTerms}
+            </Text>
+          </View>
+        )}
+        {hasDueDate && (
+          <View style={{ minWidth: '40%' }}>
+            <Text style={{ fontSize: 8, color: '#6b7280', marginBottom: 2 }}>
+              {labels.dueDateLabel || 'Due Date'}
+            </Text>
+            <Text style={{ fontSize: 10, fontFamily: fontBold }}>
+              {dueDate}
+            </Text>
+          </View>
+        )}
+      </View>
+    </View>
+  )
+}
+
+export function DiagnosticNotesSection({
+  diagnosticNotes,
+  fontFamily,
+  styles,
+  labels,
+}: {
+  diagnosticNotes: string | null
+  fontFamily: string
+  styles: Record<string, Style>
+  labels: Record<string, string>
+}) {
+  const fontBold = getFontBold(fontFamily)
+  if (!hasContent(diagnosticNotes)) return null
+  return (
+    <View wrap={false} style={{ ...styles.notesSection, marginTop: 8 }}>
+      <Text style={styles.notesLabel}>{labels.diagnosticNotes || 'Diagnostic Notes'}</Text>
+      <HtmlToPdf html={diagnosticNotes!} baseStyle={styles.notesText} fontBold={fontBold} />
+    </View>
+  )
+}
+
+// ---------------------------------------------------------------------------
+// Composite Notes component – kept for backward compatibility
+// ---------------------------------------------------------------------------
+
+export function Notes({
+  invoiceNotes,
+  diagnosticNotes,
+  invoiceSettings,
+  otherAttachments,
+  pdfAttachmentNames,
+  fontFamily,
+  styles,
+  labels,
+}: NotesProps) {
+  return (
+    <>
+      <NotesOnly
+        invoiceNotes={invoiceNotes}
+        otherAttachments={otherAttachments}
+        pdfAttachmentNames={pdfAttachmentNames}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+      <BankAccountSection
+        invoiceSettings={invoiceSettings}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+      <DiagnosticNotesSection
+        diagnosticNotes={diagnosticNotes}
+        fontFamily={fontFamily}
+        styles={styles}
+        labels={labels}
+      />
+    </>
+  )
+}

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

@@ -49,7 +49,7 @@ export function createStyles(primary: string, font: string) {
     invoiceTitle: { fontSize: 18, fontFamily: fontBold, textAlign: 'right' as const },
     invoiceTitle: { fontSize: 18, fontFamily: fontBold, textAlign: 'right' as const },
     invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
     invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
     infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
     infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
-    infoBox: { flex: 1, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
+    infoBox: { padding: 12, backgroundColor: grayLight, borderRadius: 4, marginBottom: 4 },
     infoLabel: {
     infoLabel: {
       fontSize: 8,
       fontSize: 8,
       fontFamily: fontBold,
       fontFamily: fontBold,

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

@@ -1,9 +1,13 @@
+import type { InvoiceLayoutConfig } from "@/features/settings/Schema/invoiceLayoutSchema";
+
 export interface TemplateConfig {
 export interface TemplateConfig {
   primaryColor?: string
   primaryColor?: string
   fontFamily?: string
   fontFamily?: string
   showLogo?: boolean
   showLogo?: boolean
   showCompanyName?: boolean
   showCompanyName?: boolean
   headerStyle?: string
   headerStyle?: string
+  logoSize?: number
+  layoutConfig?: InvoiceLayoutConfig
 }
 }
 
 
 export interface InvoiceData {
 export interface InvoiceData {
@@ -39,6 +43,7 @@ export interface InvoiceData {
     rate: number
     rate: number
     total: number
     total: number
   }[]
   }[]
+  customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>;
   vehicle: {
   vehicle: {
     make: string
     make: string
     model: string
     model: string

+ 2 - 2
src/features/vehicles/Components/service-detail/ServiceSidebar.tsx

@@ -3,7 +3,7 @@
 import Link from "next/link";
 import Link from "next/link";
 import { useTranslations } from "next-intl";
 import { useTranslations } from "next-intl";
 import { Building2, Car, Mail, MapPin, Phone, Users } from "lucide-react";
 import { Building2, Car, Mail, MapPin, Phone, Users } from "lucide-react";
-import { CustomFieldsDisplay } from "@/features/custom-fields/Components/CustomFieldsDisplay";
+import { CustomFieldsForm } from "@/features/custom-fields/Components/CustomFieldsForm";
 import type { Vehicle } from "./types";
 import type { Vehicle } from "./types";
 
 
 interface ServiceSidebarProps {
 interface ServiceSidebarProps {
@@ -89,7 +89,7 @@ export function ServiceSidebar({
       )}
       )}
 
 
       {/* Custom Fields */}
       {/* Custom Fields */}
-      <CustomFieldsDisplay entityId={recordId} entityType="service_record" />
+      <CustomFieldsForm entityId={recordId} entityType="service_record" />
     </>
     </>
   );
   );
 }
 }

+ 7 - 0
src/features/vehicles/Components/service-page/DetailsRightColumn.tsx

@@ -4,6 +4,7 @@ import { BasicInfoSection } from '../service-edit/BasicInfoSection'
 import { ScheduleTimesSection } from '../service-edit/ScheduleTimesSection'
 import { ScheduleTimesSection } from '../service-edit/ScheduleTimesSection'
 import { TotalsSection } from '../service-edit/TotalsSection'
 import { TotalsSection } from '../service-edit/TotalsSection'
 import { ServiceAttachments } from '../service-detail/ServiceAttachments'
 import { ServiceAttachments } from '../service-detail/ServiceAttachments'
+import { CustomFieldsForm } from '@/features/custom-fields/Components/CustomFieldsForm'
 import { revokePublicLink } from '@/features/vehicles/Actions/serviceActions'
 import { revokePublicLink } from '@/features/vehicles/Actions/serviceActions'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceActions } from './useServiceActions'
 import type { useServiceActions } from './useServiceActions'
@@ -99,6 +100,12 @@ export function DetailsRightColumn({
         onDeleteAttachment={actions.handleDeleteAttachment}
         onDeleteAttachment={actions.handleDeleteAttachment}
         deletingAttachment={actions.deletingAttachment}
         deletingAttachment={actions.deletingAttachment}
       />
       />
+      <CustomFieldsForm
+        entityId={record.id}
+        entityType="service_record"
+        onValuesReady={formState.onCustomFieldsReady}
+        onChange={formState.markDirty}
+      />
     </div>
     </div>
   )
   )
 }
 }

+ 18 - 2
src/features/vehicles/Components/service-page/useServiceActions.ts

@@ -48,19 +48,35 @@ export function useServiceActions({
     discountType, discountValue, discountAmount,
     discountType, discountValue, discountAmount,
     isSavingRef, autosaveTimer, setLoading,
     isSavingRef, autosaveTimer, setLoading,
     setHasUnsavedChanges, flashSaved, notesRef,
     setHasUnsavedChanges, flashSaved, notesRef,
-    initialData,
+    initialData, customFieldsSaveRef,
   } = formState
   } = formState
 
 
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault()
     e.preventDefault()
+    const form = e.currentTarget
     if (isSavingRef.current) return
     if (isSavingRef.current) return
     if (autosaveTimer.current) clearTimeout(autosaveTimer.current)
     if (autosaveTimer.current) clearTimeout(autosaveTimer.current)
+
+    // Validate and save custom fields before saving
+    if (customFieldsSaveRef.current) {
+      try {
+        const { valid } = await customFieldsSaveRef.current()
+        if (!valid) {
+          toast.error(t('page.customFieldsInvalid'))
+          return
+        }
+      } catch (err) {
+        console.error('Custom fields save error:', err)
+        toast.error(t('page.customFieldsSaveFailed'))
+        return
+      }
+    }
+
     isSavingRef.current = true
     isSavingRef.current = true
     setLoading(true)
     setLoading(true)
 
 
     // Mobile + desktop layouts both render inputs with the same name.
     // Mobile + desktop layouts both render inputs with the same name.
     // formData.get() returns the first (hidden/stale) one, so read the visible input via offsetParent.
     // formData.get() returns the first (hidden/stale) one, so read the visible input via offsetParent.
-    const form = e.currentTarget
     const getVisible = (name: string) => {
     const getVisible = (name: string) => {
       const inputs = form.querySelectorAll<HTMLInputElement>(`input[name="${name}"]`)
       const inputs = form.querySelectorAll<HTMLInputElement>(`input[name="${name}"]`)
       const visible = Array.from(inputs).find(el => el.offsetParent !== null)
       const visible = Array.from(inputs).find(el => el.offsetParent !== null)

+ 9 - 0
src/features/vehicles/Components/service-page/useServiceFormState.ts

@@ -73,6 +73,13 @@ export function useServiceFormState({
     })
     })
   }
   }
 
 
+  // Custom fields save callback ref
+  const customFieldsSaveRef = useRef<(() => Promise<{ valid: boolean }>) | null>(null)
+
+  const onCustomFieldsReady = useCallback((save: () => Promise<{ valid: boolean }>) => {
+    customFieldsSaveRef.current = save
+  }, [])
+
   // Notes ref
   // Notes ref
   const notesRef = useRef({
   const notesRef = useRef({
     invoiceNotes: initialData.invoiceNotes || '',
     invoiceNotes: initialData.invoiceNotes || '',
@@ -180,6 +187,8 @@ export function useServiceFormState({
     flashSaved, markDirty, saveNow,
     flashSaved, markDirty, saveNow,
     // Notes
     // Notes
     notesRef, handleNotesChange,
     notesRef, handleNotesChange,
+    // Custom fields
+    customFieldsSaveRef, onCustomFieldsReady,
     // Computed
     // Computed
     partsSubtotal, laborSubtotal, subtotal,
     partsSubtotal, laborSubtotal, subtotal,
     discountAmount, taxAmount, totalAmount,
     discountAmount, taxAmount, totalAmount,