Procházet zdrojové kódy

Add service/flat-fee pricing type for labor items (#94)

* service units

* units

* button

* test
Bernt Christian Egeland před 6 měsíci
rodič
revize
ae84e0715c
81 změnil soubory, kde provedl 818 přidání a 218 odebrání
  1. 7 2
      messages/de/laborPresets.json
  2. 8 2
      messages/de/pdf.json
  3. 10 1
      messages/de/quotes.json
  4. 10 1
      messages/de/service.json
  5. 4 0
      messages/de/share.json
  6. 11 3
      messages/en/laborPresets.json
  7. 8 2
      messages/en/pdf.json
  8. 11 2
      messages/en/quotes.json
  9. 11 2
      messages/en/service.json
  10. 8 2
      messages/en/share.json
  11. 7 2
      messages/es/laborPresets.json
  12. 8 2
      messages/es/pdf.json
  13. 10 1
      messages/es/quotes.json
  14. 10 1
      messages/es/service.json
  15. 4 0
      messages/es/share.json
  16. 7 2
      messages/fr/laborPresets.json
  17. 8 2
      messages/fr/pdf.json
  18. 10 1
      messages/fr/quotes.json
  19. 10 1
      messages/fr/service.json
  20. 4 0
      messages/fr/share.json
  21. 7 2
      messages/it/laborPresets.json
  22. 8 2
      messages/it/pdf.json
  23. 10 1
      messages/it/quotes.json
  24. 10 1
      messages/it/service.json
  25. 4 0
      messages/it/share.json
  26. 7 2
      messages/nb/laborPresets.json
  27. 8 2
      messages/nb/pdf.json
  28. 10 1
      messages/nb/quotes.json
  29. 11 2
      messages/nb/service.json
  30. 4 0
      messages/nb/share.json
  31. 11 3
      messages/nl/laborPresets.json
  32. 8 2
      messages/nl/pdf.json
  33. 10 1
      messages/nl/quotes.json
  34. 10 1
      messages/nl/service.json
  35. 4 0
      messages/nl/share.json
  36. 7 2
      messages/pl/laborPresets.json
  37. 8 2
      messages/pl/pdf.json
  38. 10 1
      messages/pl/quotes.json
  39. 10 1
      messages/pl/service.json
  40. 4 0
      messages/pl/share.json
  41. 7 2
      messages/pt-BR/laborPresets.json
  42. 8 2
      messages/pt-BR/pdf.json
  43. 10 1
      messages/pt-BR/quotes.json
  44. 10 1
      messages/pt-BR/service.json
  45. 4 0
      messages/pt-BR/share.json
  46. 7 2
      messages/ru/laborPresets.json
  47. 8 2
      messages/ru/pdf.json
  48. 10 1
      messages/ru/quotes.json
  49. 10 1
      messages/ru/service.json
  50. 4 0
      messages/ru/share.json
  51. 7 2
      messages/tr/laborPresets.json
  52. 8 2
      messages/tr/pdf.json
  53. 10 1
      messages/tr/quotes.json
  54. 10 1
      messages/tr/service.json
  55. 4 0
      messages/tr/share.json
  56. 11 0
      prisma/migrations/20260326062211_service_unit/migration.sql
  57. 4 0
      prisma/schema.prisma
  58. 1 1
      src/__tests__/features/public/invoice-view.test.tsx
  59. 2 2
      src/__tests__/features/public/quote-view.test.tsx
  60. 26 2
      src/app/(authenticated)/labor-presets/labor-presets-client.tsx
  61. 1 0
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx
  62. 4 3
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  63. 4 3
      src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx
  64. 4 0
      src/features/labor-presets/Actions/laborPresetActions.ts
  65. 68 29
      src/features/labor-presets/Components/LaborPresetForm.tsx
  66. 20 8
      src/features/labor-presets/Components/LaborPresetPickerDialog.tsx
  67. 1 0
      src/features/labor-presets/Schema/laborPresetSchema.ts
  68. 1 0
      src/features/quotes/Actions/quoteActions.ts
  69. 52 7
      src/features/quotes/Components/QuoteLaborEditor.tsx
  70. 25 16
      src/features/quotes/Components/QuotePDF.tsx
  71. 4 2
      src/features/quotes/Components/QuotePageClient.tsx
  72. 11 1
      src/features/quotes/Components/quote-page-types.ts
  73. 8 3
      src/features/quotes/Components/useQuoteFormState.ts
  74. 1 0
      src/features/quotes/Schema/quoteSchema.ts
  75. 24 15
      src/features/vehicles/Components/invoice-pdf/Tables.tsx
  76. 1 0
      src/features/vehicles/Components/invoice-pdf/types.ts
  77. 87 50
      src/features/vehicles/Components/service-edit/LaborEditor.tsx
  78. 9 0
      src/features/vehicles/Components/service-edit/form-types.ts
  79. 3 2
      src/features/vehicles/Components/service-page/ServicePageClient.tsx
  80. 1 0
      src/features/vehicles/Schema/serviceSchema.ts
  81. 1 1
      src/lib/cron/recurring-invoices.ts

+ 7 - 2
messages/de/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Position hinzufügen",
     "addItem": "Position hinzufügen",
     "cancel": "Abbrechen",
     "cancel": "Abbrechen",
     "create": "Erstellen",
     "create": "Erstellen",
-    "saveChanges": "Änderungen speichern"
+    "saveChanges": "Änderungen speichern",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Arbeitsvorlage auswählen",
     "title": "Arbeitsvorlage auswählen",
     "search": "Vorlagen durchsuchen...",
     "search": "Vorlagen durchsuchen...",
     "noPackages": "Keine Arbeitsvorlagen gefunden.",
     "noPackages": "Keine Arbeitsvorlagen gefunden.",
     "items": "{count} Positionen",
     "items": "{count} Positionen",
-    "totalHours": "{hours} Std. gesamt"
+    "totalHours": "{hours} Std. gesamt",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Fehler",
     "error": "Fehler",

+ 8 - 2
messages/de/pdf.json

@@ -21,6 +21,8 @@
     "total": "Gesamt",
     "total": "Gesamt",
     "labor": "Arbeitsleistung",
     "labor": "Arbeitsleistung",
     "hours": "Stunden",
     "hours": "Stunden",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stundensatz",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",
     "subtotal": "Zwischensumme",
@@ -44,7 +46,8 @@
     "viewPortal": "Ihr Portal ansehen: {url}",
     "viewPortal": "Ihr Portal ansehen: {url}",
     "professionalWorkshop": "Professionelle Werkstattverwaltung",
     "professionalWorkshop": "Professionelle Werkstattverwaltung",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "ANGEBOT",
     "title": "ANGEBOT",
@@ -61,6 +64,8 @@
     "total": "Gesamt",
     "total": "Gesamt",
     "labor": "Arbeitsleistung",
     "labor": "Arbeitsleistung",
     "hours": "Stunden",
     "hours": "Stunden",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stundensatz",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",
     "subtotal": "Zwischensumme",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Dieses Angebot ist gültig bis {date}",
     "validityFooterUntil": "Dieses Angebot ist gültig bis {date}",
     "validityFooter30": "Dieses Angebot ist 30 Tage gültig",
     "validityFooter30": "Dieses Angebot ist 30 Tage gültig",
     "viewPortal": "Ihr Portal ansehen: {url}",
     "viewPortal": "Ihr Portal ansehen: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "FAHRZEUGINSPEKTION",
     "title": "FAHRZEUGINSPEKTION",

+ 10 - 1
messages/de/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Stunden",
     "hours": "Stunden",
     "rate": "Satz ({currency}/Std.)",
     "rate": "Satz ({currency}/Std.)",
     "total": "Gesamt",
     "total": "Gesamt",
-    "subtotal": "Arbeitszeit Zwischensumme: {amount}"
+    "subtotal": "Arbeitszeit Zwischensumme: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notizen",
     "title": "Notizen",

+ 10 - 1
messages/de/service.json

@@ -92,7 +92,16 @@
     "rate": "Satz ({currency}/Std.)",
     "rate": "Satz ({currency}/Std.)",
     "total": "Gesamt",
     "total": "Gesamt",
     "descriptionPlaceholder": "Beschreibung *",
     "descriptionPlaceholder": "Beschreibung *",
-    "subtotal": "Arbeitssumme: {amount}"
+    "subtotal": "Arbeitssumme: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Summen",
     "title": "Summen",

+ 4 - 0
messages/de/share.json

@@ -33,6 +33,8 @@
     "total": "Gesamt",
     "total": "Gesamt",
     "labor": "Arbeitsleistung",
     "labor": "Arbeitsleistung",
     "hours": "Stunden",
     "hours": "Stunden",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stundensatz",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",
     "subtotal": "Zwischensumme",
@@ -72,6 +74,8 @@
     "labor": "Arbeitsleistung",
     "labor": "Arbeitsleistung",
     "laborDescription": "Beschreibung",
     "laborDescription": "Beschreibung",
     "hours": "Stunden",
     "hours": "Stunden",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stundensatz",
     "rate": "Stundensatz",
     "ratePerHour": "{rate}/Std.",
     "ratePerHour": "{rate}/Std.",
     "subtotal": "Zwischensumme",
     "subtotal": "Zwischensumme",

+ 11 - 3
messages/en/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
   "table": {
     "name": "Name",
     "name": "Name",
     "description": "Description",
     "description": "Description",
-    "itemCount": "Items"
+    "itemCount": "Items",
+    "type": "Type",
+    "hrs": "hrs",
+    "units": "unit(s)"
   },
   },
   "empty": {
   "empty": {
     "noMatch": "No packages match your search.",
     "noMatch": "No packages match your search.",
@@ -33,19 +36,24 @@
     "itemsTitle": "Labor Items",
     "itemsTitle": "Labor Items",
     "itemDescription": "Description",
     "itemDescription": "Description",
     "itemHours": "Hours",
     "itemHours": "Hours",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
     "itemRate": "Rate",
     "itemRate": "Rate",
     "itemDescriptionPlaceholder": "Description *",
     "itemDescriptionPlaceholder": "Description *",
     "addItem": "Add Item",
     "addItem": "Add Item",
     "cancel": "Cancel",
     "cancel": "Cancel",
     "create": "Create Package",
     "create": "Create Package",
-    "saveChanges": "Save Changes"
+    "saveChanges": "Save Changes",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Select Labor Package",
     "title": "Select Labor Package",
     "search": "Search packages...",
     "search": "Search packages...",
     "noPackages": "No labor packages found.",
     "noPackages": "No labor packages found.",
     "items": "{count} items",
     "items": "{count} items",
-    "totalHours": "{hours}h total"
+    "totalHours": "{hours}h total",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Error",
     "error": "Error",

+ 8 - 2
messages/en/pdf.json

@@ -19,8 +19,11 @@
     "qty": "Qty",
     "qty": "Qty",
     "unitPrice": "Unit Price",
     "unitPrice": "Unit Price",
     "total": "Total",
     "total": "Total",
-    "labor": "Labor",
+    "labor": "Labor & Services",
     "hours": "Hours",
     "hours": "Hours",
+    "hrs": "hrs",
+    "unit": "unit",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate",
     "rate": "Rate",
     "ratePerHour": "{rate}/hr",
     "ratePerHour": "{rate}/hr",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -59,8 +62,11 @@
     "qty": "Qty",
     "qty": "Qty",
     "unitPrice": "Unit Price",
     "unitPrice": "Unit Price",
     "total": "Total",
     "total": "Total",
-    "labor": "Labor",
+    "labor": "Labor & Services",
     "hours": "Hours",
     "hours": "Hours",
+    "hrs": "hrs",
+    "unit": "unit",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate",
     "rate": "Rate",
     "ratePerHour": "{rate}/hr",
     "ratePerHour": "{rate}/hr",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",

+ 11 - 2
messages/en/quotes.json

@@ -73,15 +73,24 @@
     "subtotal": "Parts Subtotal: {amount}"
     "subtotal": "Parts Subtotal: {amount}"
   },
   },
   "labor": {
   "labor": {
-    "title": "Labor",
+    "title": "Labor & Services",
     "addLabor": "Add Labor",
     "addLabor": "Add Labor",
+    "addService": "Add Service",
     "fromPresets": "From Presets",
     "fromPresets": "From Presets",
     "description": "Description",
     "description": "Description",
     "descriptionPlaceholder": "Description *",
     "descriptionPlaceholder": "Description *",
     "hours": "Hours",
     "hours": "Hours",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate ({currency}/hr)",
     "rate": "Rate ({currency}/hr)",
     "total": "Total",
     "total": "Total",
-    "subtotal": "Labor Subtotal: {amount}"
+    "subtotal": "Labor Subtotal: {amount}",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours \u00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notes",
     "title": "Notes",

+ 11 - 2
messages/en/service.json

@@ -84,15 +84,24 @@
     "partNotFound": "No part found for barcode \"{barcode}\""
     "partNotFound": "No part found for barcode \"{barcode}\""
   },
   },
   "labor": {
   "labor": {
-    "title": "Labor",
+    "title": "Labor & Services",
     "addLabor": "Add Labor",
     "addLabor": "Add Labor",
+    "addService": "Add Service",
     "fromPresets": "From Presets",
     "fromPresets": "From Presets",
     "description": "Description",
     "description": "Description",
     "hours": "Hours",
     "hours": "Hours",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate ({currency}/hr)",
     "rate": "Rate ({currency}/hr)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Description *",
     "descriptionPlaceholder": "Description *",
-    "subtotal": "Labor Subtotal: {amount}"
+    "subtotal": "Labor Subtotal: {amount}",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours \u00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totals",
     "title": "Totals",

+ 8 - 2
messages/en/share.json

@@ -31,8 +31,11 @@
     "qty": "Qty",
     "qty": "Qty",
     "unitPrice": "Unit Price",
     "unitPrice": "Unit Price",
     "total": "Total",
     "total": "Total",
-    "labor": "Labor",
+    "labor": "Labor & Services",
     "hours": "Hours",
     "hours": "Hours",
+    "hrs": "hrs",
+    "unit": "unit",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate",
     "rate": "Rate",
     "ratePerHour": "{rate}/hr",
     "ratePerHour": "{rate}/hr",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -69,9 +72,12 @@
     "qty": "Qty",
     "qty": "Qty",
     "unitPrice": "Unit Price",
     "unitPrice": "Unit Price",
     "total": "Total",
     "total": "Total",
-    "labor": "Labor",
+    "labor": "Labor & Services",
     "laborDescription": "Description",
     "laborDescription": "Description",
     "hours": "Hours",
     "hours": "Hours",
+    "hrs": "hrs",
+    "unit": "unit",
+    "qtyOrHours": "Unit / Hours",
     "rate": "Rate",
     "rate": "Rate",
     "ratePerHour": "{rate}/hr",
     "ratePerHour": "{rate}/hr",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",

+ 7 - 2
messages/es/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Agregar elemento",
     "addItem": "Agregar elemento",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
     "create": "Crear",
     "create": "Crear",
-    "saveChanges": "Guardar cambios"
+    "saveChanges": "Guardar cambios",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Seleccionar plantilla de mano de obra",
     "title": "Seleccionar plantilla de mano de obra",
     "search": "Buscar plantillas...",
     "search": "Buscar plantillas...",
     "noPackages": "No se encontraron plantillas de mano de obra.",
     "noPackages": "No se encontraron plantillas de mano de obra.",
     "items": "{count} elementos",
     "items": "{count} elementos",
-    "totalHours": "{hours}h en total"
+    "totalHours": "{hours}h en total",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Error",
     "error": "Error",

+ 8 - 2
messages/es/pdf.json

@@ -21,6 +21,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mano de obra",
     "labor": "Mano de obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarifa",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -44,7 +46,8 @@
     "viewPortal": "Ver su portal: {url}",
     "viewPortal": "Ver su portal: {url}",
     "professionalWorkshop": "Gestión profesional de talleres",
     "professionalWorkshop": "Gestión profesional de talleres",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "PRESUPUESTO",
     "title": "PRESUPUESTO",
@@ -61,6 +64,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mano de obra",
     "labor": "Mano de obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarifa",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Este presupuesto es válido hasta el {date}",
     "validityFooterUntil": "Este presupuesto es válido hasta el {date}",
     "validityFooter30": "Este presupuesto es válido por 30 días",
     "validityFooter30": "Este presupuesto es válido por 30 días",
     "viewPortal": "Ver su portal: {url}",
     "viewPortal": "Ver su portal: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "INSPECCIÓN DE VEHÍCULO",
     "title": "INSPECCIÓN DE VEHÍCULO",

+ 10 - 1
messages/es/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Horas",
     "hours": "Horas",
     "rate": "Tarifa ({currency}/hr)",
     "rate": "Tarifa ({currency}/hr)",
     "total": "Total",
     "total": "Total",
-    "subtotal": "Subtotal de mano de obra: {amount}"
+    "subtotal": "Subtotal de mano de obra: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notas",
     "title": "Notas",

+ 10 - 1
messages/es/service.json

@@ -92,7 +92,16 @@
     "rate": "Tarifa ({currency}/hr)",
     "rate": "Tarifa ({currency}/hr)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Descripción *",
     "descriptionPlaceholder": "Descripción *",
-    "subtotal": "Subtotal de Mano de Obra: {amount}"
+    "subtotal": "Subtotal de Mano de Obra: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totales",
     "title": "Totales",

+ 4 - 0
messages/es/share.json

@@ -33,6 +33,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mano de obra",
     "labor": "Mano de obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarifa",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -72,6 +74,8 @@
     "labor": "Mano de obra",
     "labor": "Mano de obra",
     "laborDescription": "Descripción",
     "laborDescription": "Descripción",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarifa",
     "rate": "Tarifa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",

+ 7 - 2
messages/fr/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Ajouter un élément",
     "addItem": "Ajouter un élément",
     "cancel": "Annuler",
     "cancel": "Annuler",
     "create": "Créer",
     "create": "Créer",
-    "saveChanges": "Enregistrer les modifications"
+    "saveChanges": "Enregistrer les modifications",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Sélectionner un modèle de main-d'œuvre",
     "title": "Sélectionner un modèle de main-d'œuvre",
     "search": "Rechercher des modèles...",
     "search": "Rechercher des modèles...",
     "noPackages": "Aucun modèle de main-d'œuvre trouvé.",
     "noPackages": "Aucun modèle de main-d'œuvre trouvé.",
     "items": "{count} éléments",
     "items": "{count} éléments",
-    "totalHours": "{hours}h au total"
+    "totalHours": "{hours}h au total",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Erreur",
     "error": "Erreur",

+ 8 - 2
messages/fr/pdf.json

@@ -21,6 +21,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Main-d'œuvre",
     "labor": "Main-d'œuvre",
     "hours": "Heures",
     "hours": "Heures",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taux",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",
     "subtotal": "Sous-total",
@@ -44,7 +46,8 @@
     "viewPortal": "Consultez votre portail : {url}",
     "viewPortal": "Consultez votre portail : {url}",
     "professionalWorkshop": "Gestion professionnelle d'atelier",
     "professionalWorkshop": "Gestion professionnelle d'atelier",
     "tel": "Tél. : {phone}",
     "tel": "Tél. : {phone}",
-    "org": "Org. : {org}"
+    "org": "Org. : {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "DEVIS",
     "title": "DEVIS",
@@ -61,6 +64,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Main-d'œuvre",
     "labor": "Main-d'œuvre",
     "hours": "Heures",
     "hours": "Heures",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taux",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",
     "subtotal": "Sous-total",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Ce devis est valable jusqu'au {date}",
     "validityFooterUntil": "Ce devis est valable jusqu'au {date}",
     "validityFooter30": "Ce devis est valable 30 jours",
     "validityFooter30": "Ce devis est valable 30 jours",
     "viewPortal": "Consultez votre portail : {url}",
     "viewPortal": "Consultez votre portail : {url}",
-    "tel": "Tél. : {phone}"
+    "tel": "Tél. : {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "INSPECTION DU VÉHICULE",
     "title": "INSPECTION DU VÉHICULE",

+ 10 - 1
messages/fr/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Heures",
     "hours": "Heures",
     "rate": "Tarif ({currency}/h)",
     "rate": "Tarif ({currency}/h)",
     "total": "Total",
     "total": "Total",
-    "subtotal": "Sous-total main-d'œuvre : {amount}"
+    "subtotal": "Sous-total main-d'œuvre : {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notes",
     "title": "Notes",

+ 10 - 1
messages/fr/service.json

@@ -92,7 +92,16 @@
     "rate": "Taux ({currency}/h)",
     "rate": "Taux ({currency}/h)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Description *",
     "descriptionPlaceholder": "Description *",
-    "subtotal": "Sous-total Main-d'œuvre: {amount}"
+    "subtotal": "Sous-total Main-d'œuvre: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totaux",
     "title": "Totaux",

+ 4 - 0
messages/fr/share.json

@@ -33,6 +33,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Main-d'œuvre",
     "labor": "Main-d'œuvre",
     "hours": "Heures",
     "hours": "Heures",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taux",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",
     "subtotal": "Sous-total",
@@ -72,6 +74,8 @@
     "labor": "Main-d'œuvre",
     "labor": "Main-d'œuvre",
     "laborDescription": "Description",
     "laborDescription": "Description",
     "hours": "Heures",
     "hours": "Heures",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taux",
     "rate": "Taux",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Sous-total",
     "subtotal": "Sous-total",

+ 7 - 2
messages/it/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Aggiungi voce",
     "addItem": "Aggiungi voce",
     "cancel": "Annulla",
     "cancel": "Annulla",
     "create": "Crea",
     "create": "Crea",
-    "saveChanges": "Salva modifiche"
+    "saveChanges": "Salva modifiche",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Seleziona modello di manodopera",
     "title": "Seleziona modello di manodopera",
     "search": "Cerca modelli...",
     "search": "Cerca modelli...",
     "noPackages": "Nessun modello di manodopera trovato.",
     "noPackages": "Nessun modello di manodopera trovato.",
     "items": "{count} voci",
     "items": "{count} voci",
-    "totalHours": "{hours}h in totale"
+    "totalHours": "{hours}h in totale",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Errore",
     "error": "Errore",

+ 8 - 2
messages/it/pdf.json

@@ -21,6 +21,8 @@
     "total": "Totale",
     "total": "Totale",
     "labor": "Manodopera",
     "labor": "Manodopera",
     "hours": "Ore",
     "hours": "Ore",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tariffa",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",
     "subtotal": "Subtotale",
@@ -44,7 +46,8 @@
     "viewPortal": "Visualizza il tuo portale: {url}",
     "viewPortal": "Visualizza il tuo portale: {url}",
     "professionalWorkshop": "Gestione Officina Professionale",
     "professionalWorkshop": "Gestione Officina Professionale",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "PREVENTIVO",
     "title": "PREVENTIVO",
@@ -61,6 +64,8 @@
     "total": "Totale",
     "total": "Totale",
     "labor": "Manodopera",
     "labor": "Manodopera",
     "hours": "Ore",
     "hours": "Ore",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tariffa",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",
     "subtotal": "Subtotale",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Questo preventivo è valido fino al {date}",
     "validityFooterUntil": "Questo preventivo è valido fino al {date}",
     "validityFooter30": "Questo preventivo è valido per 30 giorni",
     "validityFooter30": "Questo preventivo è valido per 30 giorni",
     "viewPortal": "Visualizza il tuo portale: {url}",
     "viewPortal": "Visualizza il tuo portale: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "ISPEZIONE VEICOLO",
     "title": "ISPEZIONE VEICOLO",

+ 10 - 1
messages/it/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Ore",
     "hours": "Ore",
     "rate": "Tariffa ({currency}/ora)",
     "rate": "Tariffa ({currency}/ora)",
     "total": "Totale",
     "total": "Totale",
-    "subtotal": "Subtotale manodopera: {amount}"
+    "subtotal": "Subtotale manodopera: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Note",
     "title": "Note",

+ 10 - 1
messages/it/service.json

@@ -92,7 +92,16 @@
     "rate": "Tariffa ({currency}/h)",
     "rate": "Tariffa ({currency}/h)",
     "total": "Totale",
     "total": "Totale",
     "descriptionPlaceholder": "Descrizione *",
     "descriptionPlaceholder": "Descrizione *",
-    "subtotal": "Subtotale Manodopera: {amount}"
+    "subtotal": "Subtotale Manodopera: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totali",
     "title": "Totali",

+ 4 - 0
messages/it/share.json

@@ -33,6 +33,8 @@
     "total": "Totale",
     "total": "Totale",
     "labor": "Manodopera",
     "labor": "Manodopera",
     "hours": "Ore",
     "hours": "Ore",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tariffa",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",
     "subtotal": "Subtotale",
@@ -72,6 +74,8 @@
     "labor": "Manodopera",
     "labor": "Manodopera",
     "laborDescription": "Descrizione",
     "laborDescription": "Descrizione",
     "hours": "Ore",
     "hours": "Ore",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tariffa",
     "rate": "Tariffa",
     "ratePerHour": "{rate}/ora",
     "ratePerHour": "{rate}/ora",
     "subtotal": "Subtotale",
     "subtotal": "Subtotale",

+ 7 - 2
messages/nb/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Legg til element",
     "addItem": "Legg til element",
     "cancel": "Avbryt",
     "cancel": "Avbryt",
     "create": "Opprett",
     "create": "Opprett",
-    "saveChanges": "Lagre endringer"
+    "saveChanges": "Lagre endringer",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Velg arbeidsmal",
     "title": "Velg arbeidsmal",
     "search": "Søk i maler...",
     "search": "Søk i maler...",
     "noPackages": "Ingen arbeidsmaler funnet.",
     "noPackages": "Ingen arbeidsmaler funnet.",
     "items": "{count} elementer",
     "items": "{count} elementer",
-    "totalHours": "{hours}t totalt"
+    "totalHours": "{hours}t totalt",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Feil",
     "error": "Feil",

+ 8 - 2
messages/nb/pdf.json

@@ -21,6 +21,8 @@
     "total": "Totalt",
     "total": "Totalt",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Timer",
     "hours": "Timer",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Sats",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",
     "subtotal": "Delsum",
@@ -44,7 +46,8 @@
     "viewPortal": "Se portalen din: {url}",
     "viewPortal": "Se portalen din: {url}",
     "professionalWorkshop": "Profesjonell verkstedadministrasjon",
     "professionalWorkshop": "Profesjonell verkstedadministrasjon",
     "tel": "Tlf: {phone}",
     "tel": "Tlf: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "TILBUD",
     "title": "TILBUD",
@@ -61,6 +64,8 @@
     "total": "Totalt",
     "total": "Totalt",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Timer",
     "hours": "Timer",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Sats",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",
     "subtotal": "Delsum",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Dette tilbudet er gyldig til {date}",
     "validityFooterUntil": "Dette tilbudet er gyldig til {date}",
     "validityFooter30": "Dette tilbudet er gyldig i 30 dager",
     "validityFooter30": "Dette tilbudet er gyldig i 30 dager",
     "viewPortal": "Se portalen din: {url}",
     "viewPortal": "Se portalen din: {url}",
-    "tel": "Tlf: {phone}"
+    "tel": "Tlf: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "KJØRETØYKONTROLL",
     "title": "KJØRETØYKONTROLL",

+ 10 - 1
messages/nb/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Timer",
     "hours": "Timer",
     "rate": "Sats ({currency}/t)",
     "rate": "Sats ({currency}/t)",
     "total": "Totalt",
     "total": "Totalt",
-    "subtotal": "Arbeid delsum: {amount}"
+    "subtotal": "Arbeid delsum: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notater",
     "title": "Notater",

+ 11 - 2
messages/nb/service.json

@@ -92,7 +92,16 @@
     "rate": "Sats ({currency}/time)",
     "rate": "Sats ({currency}/time)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Beskrivelse *",
     "descriptionPlaceholder": "Beskrivelse *",
-    "subtotal": "Arbeid delsum: {amount}"
+    "subtotal": "Arbeid delsum: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totalt",
     "title": "Totalt",
@@ -289,4 +298,4 @@
     "failedUpdate": "Kunne ikke oppdatere tidsplan",
     "failedUpdate": "Kunne ikke oppdatere tidsplan",
     "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag"
     "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag"
   }
   }
-}
+}

+ 4 - 0
messages/nb/share.json

@@ -33,6 +33,8 @@
     "total": "Totalt",
     "total": "Totalt",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Timer",
     "hours": "Timer",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Sats",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",
     "subtotal": "Delsum",
@@ -72,6 +74,8 @@
     "labor": "Arbeid",
     "labor": "Arbeid",
     "laborDescription": "Beskrivelse",
     "laborDescription": "Beskrivelse",
     "hours": "Timer",
     "hours": "Timer",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Sats",
     "rate": "Sats",
     "ratePerHour": "{rate}/t",
     "ratePerHour": "{rate}/t",
     "subtotal": "Delsum",
     "subtotal": "Delsum",

+ 11 - 3
messages/nl/laborPresets.json

@@ -4,7 +4,10 @@
   "table": {
   "table": {
     "name": "Naam",
     "name": "Naam",
     "description": "Beschrijving",
     "description": "Beschrijving",
-    "itemCount": "Items"
+    "itemCount": "Items",
+    "type": "Type",
+    "hrs": "hrs",
+    "units": "unit(s)"
   },
   },
   "empty": {
   "empty": {
     "noMatch": "Geen sjablonen komen overeen met uw zoekopdracht.",
     "noMatch": "Geen sjablonen komen overeen met uw zoekopdracht.",
@@ -38,14 +41,19 @@
     "addItem": "Item toevoegen",
     "addItem": "Item toevoegen",
     "cancel": "Annuleren",
     "cancel": "Annuleren",
     "create": "Aanmaken",
     "create": "Aanmaken",
-    "saveChanges": "Wijzigingen opslaan"
+    "saveChanges": "Wijzigingen opslaan",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Arbeidssjabloon selecteren",
     "title": "Arbeidssjabloon selecteren",
     "search": "Sjablonen zoeken...",
     "search": "Sjablonen zoeken...",
     "noPackages": "Geen arbeidssjablonen gevonden.",
     "noPackages": "Geen arbeidssjablonen gevonden.",
     "items": "{count} items",
     "items": "{count} items",
-    "totalHours": "{hours}u totaal"
+    "totalHours": "{hours}u totaal",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Fout",
     "error": "Fout",

+ 8 - 2
messages/nl/pdf.json

@@ -21,6 +21,8 @@
     "total": "Totaal",
     "total": "Totaal",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Uren",
     "hours": "Uren",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarief",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",
     "subtotal": "Subtotaal",
@@ -44,7 +46,8 @@
     "viewPortal": "Bekijk uw portaal: {url}",
     "viewPortal": "Bekijk uw portaal: {url}",
     "professionalWorkshop": "Professioneel werkplaatsbeheer",
     "professionalWorkshop": "Professioneel werkplaatsbeheer",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "KvK: {org}"
+    "org": "KvK: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "OFFERTE",
     "title": "OFFERTE",
@@ -61,6 +64,8 @@
     "total": "Totaal",
     "total": "Totaal",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Uren",
     "hours": "Uren",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarief",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",
     "subtotal": "Subtotaal",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Deze offerte is geldig tot {date}",
     "validityFooterUntil": "Deze offerte is geldig tot {date}",
     "validityFooter30": "Deze offerte is 30 dagen geldig",
     "validityFooter30": "Deze offerte is 30 dagen geldig",
     "viewPortal": "Bekijk uw portaal: {url}",
     "viewPortal": "Bekijk uw portaal: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "VOERTUIGINSPECTIE",
     "title": "VOERTUIGINSPECTIE",

+ 10 - 1
messages/nl/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Uren",
     "hours": "Uren",
     "rate": "Tarief ({currency}/uur)",
     "rate": "Tarief ({currency}/uur)",
     "total": "Totaal",
     "total": "Totaal",
-    "subtotal": "Subtotaal arbeid: {amount}"
+    "subtotal": "Subtotaal arbeid: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notities",
     "title": "Notities",

+ 10 - 1
messages/nl/service.json

@@ -92,7 +92,16 @@
     "rate": "Tarief ({currency}/uur)",
     "rate": "Tarief ({currency}/uur)",
     "total": "Totaal",
     "total": "Totaal",
     "descriptionPlaceholder": "Beschrijving *",
     "descriptionPlaceholder": "Beschrijving *",
-    "subtotal": "Arbeidskosten totaal: {amount}"
+    "subtotal": "Arbeidskosten totaal: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totalen",
     "title": "Totalen",

+ 4 - 0
messages/nl/share.json

@@ -33,6 +33,8 @@
     "total": "Totaal",
     "total": "Totaal",
     "labor": "Arbeid",
     "labor": "Arbeid",
     "hours": "Uren",
     "hours": "Uren",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarief",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",
     "subtotal": "Subtotaal",
@@ -72,6 +74,8 @@
     "labor": "Arbeid",
     "labor": "Arbeid",
     "laborDescription": "Omschrijving",
     "laborDescription": "Omschrijving",
     "hours": "Uren",
     "hours": "Uren",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Tarief",
     "rate": "Tarief",
     "ratePerHour": "{rate}/uur",
     "ratePerHour": "{rate}/uur",
     "subtotal": "Subtotaal",
     "subtotal": "Subtotaal",

+ 7 - 2
messages/pl/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Dodaj pozycję",
     "addItem": "Dodaj pozycję",
     "cancel": "Anuluj",
     "cancel": "Anuluj",
     "create": "Utwórz",
     "create": "Utwórz",
-    "saveChanges": "Zapisz zmiany"
+    "saveChanges": "Zapisz zmiany",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Wybierz szablon robocizny",
     "title": "Wybierz szablon robocizny",
     "search": "Szukaj szablonów...",
     "search": "Szukaj szablonów...",
     "noPackages": "Nie znaleziono szablonów robocizny.",
     "noPackages": "Nie znaleziono szablonów robocizny.",
     "items": "{count} pozycji",
     "items": "{count} pozycji",
-    "totalHours": "{hours} godz. łącznie"
+    "totalHours": "{hours} godz. łącznie",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Błąd",
     "error": "Błąd",

+ 8 - 2
messages/pl/pdf.json

@@ -21,6 +21,8 @@
     "total": "Suma",
     "total": "Suma",
     "labor": "Robocizna",
     "labor": "Robocizna",
     "hours": "Godziny",
     "hours": "Godziny",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stawka",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",
     "subtotal": "Suma częściowa",
@@ -44,7 +46,8 @@
     "viewPortal": "Zobacz swój portal: {url}",
     "viewPortal": "Zobacz swój portal: {url}",
     "professionalWorkshop": "Profesjonalne zarządzanie warsztatem",
     "professionalWorkshop": "Profesjonalne zarządzanie warsztatem",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "WYCENA",
     "title": "WYCENA",
@@ -61,6 +64,8 @@
     "total": "Suma",
     "total": "Suma",
     "labor": "Robocizna",
     "labor": "Robocizna",
     "hours": "Godziny",
     "hours": "Godziny",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stawka",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",
     "subtotal": "Suma częściowa",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Ta wycena jest ważna do {date}",
     "validityFooterUntil": "Ta wycena jest ważna do {date}",
     "validityFooter30": "Ta wycena jest ważna przez 30 dni",
     "validityFooter30": "Ta wycena jest ważna przez 30 dni",
     "viewPortal": "Zobacz swój portal: {url}",
     "viewPortal": "Zobacz swój portal: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "INSPEKCJA POJAZDU",
     "title": "INSPEKCJA POJAZDU",

+ 10 - 1
messages/pl/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Godziny",
     "hours": "Godziny",
     "rate": "Stawka ({currency}/godz.)",
     "rate": "Stawka ({currency}/godz.)",
     "total": "Suma",
     "total": "Suma",
-    "subtotal": "Robocizna suma częściowa: {amount}"
+    "subtotal": "Robocizna suma częściowa: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notatki",
     "title": "Notatki",

+ 10 - 1
messages/pl/service.json

@@ -92,7 +92,16 @@
     "rate": "Stawka ({currency}/godz.)",
     "rate": "Stawka ({currency}/godz.)",
     "total": "Razem",
     "total": "Razem",
     "descriptionPlaceholder": "Opis *",
     "descriptionPlaceholder": "Opis *",
-    "subtotal": "Razem Praca: {amount}"
+    "subtotal": "Razem Praca: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Razem",
     "title": "Razem",

+ 4 - 0
messages/pl/share.json

@@ -33,6 +33,8 @@
     "total": "Suma",
     "total": "Suma",
     "labor": "Robocizna",
     "labor": "Robocizna",
     "hours": "Godziny",
     "hours": "Godziny",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stawka",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",
     "subtotal": "Suma częściowa",
@@ -72,6 +74,8 @@
     "labor": "Robocizna",
     "labor": "Robocizna",
     "laborDescription": "Opis",
     "laborDescription": "Opis",
     "hours": "Godziny",
     "hours": "Godziny",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Stawka",
     "rate": "Stawka",
     "ratePerHour": "{rate}/godz.",
     "ratePerHour": "{rate}/godz.",
     "subtotal": "Suma częściowa",
     "subtotal": "Suma częściowa",

+ 7 - 2
messages/pt-BR/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Adicionar item",
     "addItem": "Adicionar item",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
     "create": "Criar",
     "create": "Criar",
-    "saveChanges": "Salvar alterações"
+    "saveChanges": "Salvar alterações",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Selecionar modelo de mão de obra",
     "title": "Selecionar modelo de mão de obra",
     "search": "Pesquisar modelos...",
     "search": "Pesquisar modelos...",
     "noPackages": "Nenhum modelo de mão de obra encontrado.",
     "noPackages": "Nenhum modelo de mão de obra encontrado.",
     "items": "{count} itens",
     "items": "{count} itens",
-    "totalHours": "{hours}h no total"
+    "totalHours": "{hours}h no total",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Erro",
     "error": "Erro",

+ 8 - 2
messages/pt-BR/pdf.json

@@ -21,6 +21,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mão de Obra",
     "labor": "Mão de Obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taxa",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -44,7 +46,8 @@
     "viewPortal": "Acesse seu portal: {url}",
     "viewPortal": "Acesse seu portal: {url}",
     "professionalWorkshop": "Gestão Profissional de Oficina",
     "professionalWorkshop": "Gestão Profissional de Oficina",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "CNPJ: {org}"
+    "org": "CNPJ: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "ORÇAMENTO",
     "title": "ORÇAMENTO",
@@ -61,6 +64,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mão de Obra",
     "labor": "Mão de Obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taxa",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Este orçamento é válido até {date}",
     "validityFooterUntil": "Este orçamento é válido até {date}",
     "validityFooter30": "Este orçamento é válido por 30 dias",
     "validityFooter30": "Este orçamento é válido por 30 dias",
     "viewPortal": "Acesse seu portal: {url}",
     "viewPortal": "Acesse seu portal: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "INSPEÇÃO VEICULAR",
     "title": "INSPEÇÃO VEICULAR",

+ 10 - 1
messages/pt-BR/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Horas",
     "hours": "Horas",
     "rate": "Taxa ({currency}/h)",
     "rate": "Taxa ({currency}/h)",
     "total": "Total",
     "total": "Total",
-    "subtotal": "Subtotal de mão de obra: {amount}"
+    "subtotal": "Subtotal de mão de obra: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notas",
     "title": "Notas",

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

@@ -92,7 +92,16 @@
     "rate": "Taxa ({currency}/h)",
     "rate": "Taxa ({currency}/h)",
     "total": "Total",
     "total": "Total",
     "descriptionPlaceholder": "Descrição *",
     "descriptionPlaceholder": "Descrição *",
-    "subtotal": "Subtotal de Mão de Obra: {amount}"
+    "subtotal": "Subtotal de Mão de Obra: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Totais",
     "title": "Totais",

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

@@ -33,6 +33,8 @@
     "total": "Total",
     "total": "Total",
     "labor": "Mão de Obra",
     "labor": "Mão de Obra",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taxa",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",
@@ -72,6 +74,8 @@
     "labor": "Mão de Obra",
     "labor": "Mão de Obra",
     "laborDescription": "Descrição",
     "laborDescription": "Descrição",
     "hours": "Horas",
     "hours": "Horas",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Taxa",
     "rate": "Taxa",
     "ratePerHour": "{rate}/h",
     "ratePerHour": "{rate}/h",
     "subtotal": "Subtotal",
     "subtotal": "Subtotal",

+ 7 - 2
messages/ru/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Добавить позицию",
     "addItem": "Добавить позицию",
     "cancel": "Отмена",
     "cancel": "Отмена",
     "create": "Создать",
     "create": "Создать",
-    "saveChanges": "Сохранить"
+    "saveChanges": "Сохранить",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "Выбрать шаблон работ",
     "title": "Выбрать шаблон работ",
     "search": "Поиск шаблонов...",
     "search": "Поиск шаблонов...",
     "noPackages": "Шаблоны работ не найдены.",
     "noPackages": "Шаблоны работ не найдены.",
     "items": "{count} поз.",
     "items": "{count} поз.",
-    "totalHours": "{hours}ч всего"
+    "totalHours": "{hours}ч всего",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Ошибка",
     "error": "Ошибка",

+ 8 - 2
messages/ru/pdf.json

@@ -21,6 +21,8 @@
     "total": "Итого",
     "total": "Итого",
     "labor": "Работа",
     "labor": "Работа",
     "hours": "Часы",
     "hours": "Часы",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ставка",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",
     "subtotal": "Подитог",
@@ -44,7 +46,8 @@
     "viewPortal": "Ваш портал: {url}",
     "viewPortal": "Ваш портал: {url}",
     "professionalWorkshop": "Профессиональное управление мастерской",
     "professionalWorkshop": "Профессиональное управление мастерской",
     "tel": "Тел: {phone}",
     "tel": "Тел: {phone}",
-    "org": "Орг: {org}"
+    "org": "Орг: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ",
     "title": "КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ",
@@ -61,6 +64,8 @@
     "total": "Итого",
     "total": "Итого",
     "labor": "Работа",
     "labor": "Работа",
     "hours": "Часы",
     "hours": "Часы",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ставка",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",
     "subtotal": "Подитог",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Это предложение действительно до {date}",
     "validityFooterUntil": "Это предложение действительно до {date}",
     "validityFooter30": "Это предложение действительно 30 дней",
     "validityFooter30": "Это предложение действительно 30 дней",
     "viewPortal": "Ваш портал: {url}",
     "viewPortal": "Ваш портал: {url}",
-    "tel": "Тел: {phone}"
+    "tel": "Тел: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "ОСМОТР ТРАНСПОРТНОГО СРЕДСТВА",
     "title": "ОСМОТР ТРАНСПОРТНОГО СРЕДСТВА",

+ 10 - 1
messages/ru/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Часы",
     "hours": "Часы",
     "rate": "Ставка ({currency}/ч)",
     "rate": "Ставка ({currency}/ч)",
     "total": "Итого",
     "total": "Итого",
-    "subtotal": "Подитог работ: {amount}"
+    "subtotal": "Подитог работ: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Заметки",
     "title": "Заметки",

+ 10 - 1
messages/ru/service.json

@@ -92,7 +92,16 @@
     "rate": "Ставка ({currency}/ч)",
     "rate": "Ставка ({currency}/ч)",
     "total": "Итого",
     "total": "Итого",
     "descriptionPlaceholder": "Описание *",
     "descriptionPlaceholder": "Описание *",
-    "subtotal": "Подитог работ: {amount}"
+    "subtotal": "Подитог работ: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Итоги",
     "title": "Итоги",

+ 4 - 0
messages/ru/share.json

@@ -33,6 +33,8 @@
     "total": "Итого",
     "total": "Итого",
     "labor": "Работа",
     "labor": "Работа",
     "hours": "Часы",
     "hours": "Часы",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ставка",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",
     "subtotal": "Подитог",
@@ -72,6 +74,8 @@
     "labor": "Работа",
     "labor": "Работа",
     "laborDescription": "Описание",
     "laborDescription": "Описание",
     "hours": "Часы",
     "hours": "Часы",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ставка",
     "rate": "Ставка",
     "ratePerHour": "{rate}/ч",
     "ratePerHour": "{rate}/ч",
     "subtotal": "Подитог",
     "subtotal": "Подитог",

+ 7 - 2
messages/tr/laborPresets.json

@@ -38,14 +38,19 @@
     "addItem": "Kalem Ekle",
     "addItem": "Kalem Ekle",
     "cancel": "İptal",
     "cancel": "İptal",
     "create": "Oluştur",
     "create": "Oluştur",
-    "saveChanges": "Değişiklikleri Kaydet"
+    "saveChanges": "Değişiklikleri Kaydet",
+    "itemQty": "Qty",
+    "itemQtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC"
   },
   },
   "picker": {
   "picker": {
     "title": "İşçilik Şablonu Seç",
     "title": "İşçilik Şablonu Seç",
     "search": "Şablonları ara...",
     "search": "Şablonları ara...",
     "noPackages": "İşçilik şablonu bulunamadı.",
     "noPackages": "İşçilik şablonu bulunamadı.",
     "items": "{count} kalem",
     "items": "{count} kalem",
-    "totalHours": "toplam {hours}s"
+    "totalHours": "toplam {hours}s",
+    "totalUnits": "{units} unit(s)"
   },
   },
   "errors": {
   "errors": {
     "error": "Hata",
     "error": "Hata",

+ 8 - 2
messages/tr/pdf.json

@@ -21,6 +21,8 @@
     "total": "Toplam",
     "total": "Toplam",
     "labor": "İşçilik",
     "labor": "İşçilik",
     "hours": "Saat",
     "hours": "Saat",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ücret",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",
     "subtotal": "Ara Toplam",
@@ -44,7 +46,8 @@
     "viewPortal": "Portalınızı görüntüleyin: {url}",
     "viewPortal": "Portalınızı görüntüleyin: {url}",
     "professionalWorkshop": "Profesyonel Atölye Yönetimi",
     "professionalWorkshop": "Profesyonel Atölye Yönetimi",
     "tel": "Tel: {phone}",
     "tel": "Tel: {phone}",
-    "org": "Org: {org}"
+    "org": "Org: {org}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "quote": {
   "quote": {
     "title": "TEKLİF",
     "title": "TEKLİF",
@@ -61,6 +64,8 @@
     "total": "Toplam",
     "total": "Toplam",
     "labor": "İşçilik",
     "labor": "İşçilik",
     "hours": "Saat",
     "hours": "Saat",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ücret",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",
     "subtotal": "Ara Toplam",
@@ -74,7 +79,8 @@
     "validityFooterUntil": "Bu teklif {date} tarihine kadar geçerlidir",
     "validityFooterUntil": "Bu teklif {date} tarihine kadar geçerlidir",
     "validityFooter30": "Bu teklif 30 gün süreyle geçerlidir",
     "validityFooter30": "Bu teklif 30 gün süreyle geçerlidir",
     "viewPortal": "Portalınızı görüntüleyin: {url}",
     "viewPortal": "Portalınızı görüntüleyin: {url}",
-    "tel": "Tel: {phone}"
+    "tel": "Tel: {phone}",
+    "qtyOrHours": "Unit / Hours"
   },
   },
   "inspection": {
   "inspection": {
     "title": "ARAÇ MUAYENESİ",
     "title": "ARAÇ MUAYENESİ",

+ 10 - 1
messages/tr/quotes.json

@@ -81,7 +81,16 @@
     "hours": "Saat",
     "hours": "Saat",
     "rate": "Ücret ({currency}/sa)",
     "rate": "Ücret ({currency}/sa)",
     "total": "Toplam",
     "total": "Toplam",
-    "subtotal": "İşçilik Ara Toplam: {amount}"
+    "subtotal": "İşçilik Ara Toplam: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "notes": {
   "notes": {
     "title": "Notlar",
     "title": "Notlar",

+ 10 - 1
messages/tr/service.json

@@ -92,7 +92,16 @@
     "rate": "Ücret ({currency}/sa)",
     "rate": "Ücret ({currency}/sa)",
     "total": "Toplam",
     "total": "Toplam",
     "descriptionPlaceholder": "Açıklama *",
     "descriptionPlaceholder": "Açıklama *",
-    "subtotal": "İşçilik Ara Toplamı: {amount}"
+    "subtotal": "İşçilik Ara Toplamı: {amount}",
+    "addService": "Add Service",
+    "qty": "Qty",
+    "qtyOrHours": "Unit / Hours",
+    "hourlyTag": "HR",
+    "serviceTag": "SVC",
+    "switchToHourly": "Switch to hourly pricing",
+    "switchToService": "Switch to service pricing",
+    "switchToHourlyHint": "Hourly: priced by hours worked (hours 00d7 rate/hr). Click to switch.",
+    "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch."
   },
   },
   "totals": {
   "totals": {
     "title": "Toplamlar",
     "title": "Toplamlar",

+ 4 - 0
messages/tr/share.json

@@ -33,6 +33,8 @@
     "total": "Toplam",
     "total": "Toplam",
     "labor": "İşçilik",
     "labor": "İşçilik",
     "hours": "Saat",
     "hours": "Saat",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ücret",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",
     "subtotal": "Ara Toplam",
@@ -72,6 +74,8 @@
     "labor": "İşçilik",
     "labor": "İşçilik",
     "laborDescription": "Açıklama",
     "laborDescription": "Açıklama",
     "hours": "Saat",
     "hours": "Saat",
+    "hrs": "hrs",
+    "unit": "unit",
     "rate": "Ücret",
     "rate": "Ücret",
     "ratePerHour": "{rate}/sa",
     "ratePerHour": "{rate}/sa",
     "subtotal": "Ara Toplam",
     "subtotal": "Ara Toplam",

+ 11 - 0
prisma/migrations/20260326062211_service_unit/migration.sql

@@ -0,0 +1,11 @@
+-- AlterTable
+ALTER TABLE "labor_preset_items" ADD COLUMN     "pricingType" TEXT NOT NULL DEFAULT 'hourly';
+
+-- AlterTable
+ALTER TABLE "quote_labor" ADD COLUMN     "pricingType" TEXT NOT NULL DEFAULT 'hourly';
+
+-- AlterTable
+ALTER TABLE "recurring_labor" ADD COLUMN     "pricingType" TEXT NOT NULL DEFAULT 'hourly';
+
+-- AlterTable
+ALTER TABLE "service_labor" ADD COLUMN     "pricingType" TEXT NOT NULL DEFAULT 'hourly';

+ 4 - 0
prisma/schema.prisma

@@ -364,6 +364,7 @@ model ServiceLabor {
   hours       Float  @default(0)
   hours       Float  @default(0)
   rate        Float  @default(0)
   rate        Float  @default(0)
   total       Float  @default(0)
   total       Float  @default(0)
+  pricingType String @default("hourly")
 
 
   serviceRecordId String
   serviceRecordId String
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
@@ -393,6 +394,7 @@ model LaborPresetItem {
   description String
   description String
   hours       Float       @default(0)
   hours       Float       @default(0)
   rate        Float       @default(0)
   rate        Float       @default(0)
+  pricingType String      @default("hourly")
   sortOrder   Int         @default(0)
   sortOrder   Int         @default(0)
   presetId    String
   presetId    String
   preset      LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
   preset      LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
@@ -536,6 +538,7 @@ model QuoteLabor {
   hours       Float   @default(0)
   hours       Float   @default(0)
   rate        Float   @default(0)
   rate        Float   @default(0)
   total       Float   @default(0)
   total       Float   @default(0)
+  pricingType String  @default("hourly")
   excluded    Boolean @default(false)
   excluded    Boolean @default(false)
 
 
   quoteId String
   quoteId String
@@ -775,6 +778,7 @@ model RecurringLabor {
   description String
   description String
   hours       Float  @default(0)
   hours       Float  @default(0)
   rate        Float  @default(0)
   rate        Float  @default(0)
+  pricingType String @default("hourly")
 
 
   recurringInvoiceId String
   recurringInvoiceId String
   recurringInvoice   RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
   recurringInvoice   RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)

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

@@ -152,7 +152,7 @@ describe("InvoiceView", () => {
         },
         },
       };
       };
       render(<InvoiceView {...props} />);
       render(<InvoiceView {...props} />);
-      expect(screen.getByText("Labor")).toBeInTheDocument();
+      expect(screen.getByText("Labor & Services")).toBeInTheDocument();
       expect(screen.getByText("Brake Replacement")).toBeInTheDocument();
       expect(screen.getByText("Brake Replacement")).toBeInTheDocument();
     });
     });
 
 

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

@@ -144,8 +144,8 @@ describe("QuoteView", () => {
         },
         },
       };
       };
       render(<QuoteView {...props} />);
       render(<QuoteView {...props} />);
-      // "Labor" appears in both the table header and the totals row
-      expect(screen.getAllByText("Labor").length).toBeGreaterThanOrEqual(1);
+      // "Labor & Services" appears in both the table header and the totals row
+      expect(screen.getAllByText("Labor & Services").length).toBeGreaterThanOrEqual(1);
       expect(screen.getByText("Labor - Oil Change")).toBeInTheDocument();
       expect(screen.getByText("Labor - Oil Change")).toBeInTheDocument();
     });
     });
 
 

+ 26 - 2
src/app/(authenticated)/labor-presets/labor-presets-client.tsx

@@ -31,6 +31,7 @@ interface LaborPresetRow {
   name: string;
   name: string;
   description: string | null;
   description: string | null;
   _count: { items: number };
   _count: { items: number };
+  items: { hours: number; pricingType: string }[];
 }
 }
 
 
 interface PaginatedData {
 interface PaginatedData {
@@ -67,7 +68,7 @@ export function LaborPresetsClient({
     id: string;
     id: string;
     name: string;
     name: string;
     description: string | null;
     description: string | null;
-    items: { description: string; hours: number; rate: number; sortOrder: number }[];
+    items: { description: string; hours: number; rate: number; pricingType?: string; sortOrder: number }[];
   } | null>(null);
   } | null>(null);
   const modal = useGlassModal();
   const modal = useGlassModal();
   const confirm = useConfirm();
   const confirm = useConfirm();
@@ -181,13 +182,14 @@ export function LaborPresetsClient({
                 </button>
                 </button>
               </TableHead>
               </TableHead>
               <TableHead>{t("table.itemCount")}</TableHead>
               <TableHead>{t("table.itemCount")}</TableHead>
+              <TableHead className="hidden md:table-cell">{t("table.type")}</TableHead>
               <TableHead className="w-[50px]" />
               <TableHead className="w-[50px]" />
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>
             {data.presets.length === 0 ? (
             {data.presets.length === 0 ? (
               <TableRow>
               <TableRow>
-                <TableCell colSpan={4} className="h-32 text-center text-muted-foreground">
+                <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
                   {search ? t("empty.noMatch") : t("empty.noPresets")}
                   {search ? t("empty.noMatch") : t("empty.noPresets")}
                 </TableCell>
                 </TableCell>
               </TableRow>
               </TableRow>
@@ -203,6 +205,28 @@ export function LaborPresetsClient({
                     {preset.description || "-"}
                     {preset.description || "-"}
                   </TableCell>
                   </TableCell>
                   <TableCell>{preset._count.items}</TableCell>
                   <TableCell>{preset._count.items}</TableCell>
+                  <TableCell className="hidden md:table-cell">
+                    {(() => {
+                      const hourlyItems = preset.items.filter((i) => i.pricingType !== "service");
+                      const serviceItems = preset.items.filter((i) => i.pricingType === "service");
+                      const totalHours = hourlyItems.reduce((sum, i) => sum + i.hours, 0);
+                      const totalUnits = serviceItems.reduce((sum, i) => sum + i.hours, 0);
+                      return (
+                        <div className="flex gap-1.5">
+                          {totalHours > 0 && (
+                            <span className="inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-600">
+                              {totalHours} {t("table.hrs")}
+                            </span>
+                          )}
+                          {totalUnits > 0 && (
+                            <span className="inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium border-blue-500/30 bg-blue-500/10 text-blue-600">
+                              {totalUnits} {t("table.units")}
+                            </span>
+                          )}
+                        </div>
+                      );
+                    })()}
+                  </TableCell>
                   <TableCell onClick={(e) => e.stopPropagation()}>
                   <TableCell onClick={(e) => e.stopPropagation()}>
                     <DropdownMenu>
                     <DropdownMenu>
                       <DropdownMenuTrigger asChild>
                       <DropdownMenuTrigger asChild>

+ 1 - 0
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx

@@ -135,6 +135,7 @@ export default async function ServiceDetailPage({
       hours: l.hours,
       hours: l.hours,
       rate: l.rate,
       rate: l.rate,
       total: l.total,
       total: l.total,
+      pricingType: (l.pricingType as 'hourly' | 'service') || 'hourly',
     })),
     })),
     attachments: [],
     attachments: [],
     subtotal: record.subtotal,
     subtotal: record.subtotal,

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

@@ -53,6 +53,7 @@ interface InvoiceRecord {
     hours: number
     hours: number
     rate: number
     rate: number
     total: number
     total: number
+    pricingType?: string
   }[]
   }[]
   payments: {
   payments: {
     amount: number
     amount: number
@@ -825,7 +826,7 @@ export function InvoiceView({
                       <thead>
                       <thead>
                         <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
                         <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
                           <th className="p-2 font-medium">{t('description')}</th>
                           <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('qtyOrHours')}</th>
                           <th className="p-2 text-right font-medium">{t('rate')}</th>
                           <th className="p-2 text-right font-medium">{t('rate')}</th>
                           <th className="p-2 text-right font-medium">{t('total')}</th>
                           <th className="p-2 text-right font-medium">{t('total')}</th>
                         </tr>
                         </tr>
@@ -834,8 +835,8 @@ export function InvoiceView({
                         {record.laborItems.map((l, i) => (
                         {record.laborItems.map((l, i) => (
                           <tr key={i}>
                           <tr key={i}>
                             <td className="p-2">{l.description}</td>
                             <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">{l.pricingType === 'service' ? `${l.hours} ${t('unit')}` : `${l.hours} ${t('hrs')}`}</td>
+                            <td className="p-2 text-right">{l.pricingType === 'service' ? formatCurrency(l.rate, currencyCode) : t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
                             <td className="p-2 text-right font-medium">
                             <td className="p-2 text-right font-medium">
                               {formatCurrency(l.total, currencyCode)}
                               {formatCurrency(l.total, currencyCode)}
                             </td>
                             </td>

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

@@ -36,6 +36,7 @@ interface QuoteRecord {
     hours: number;
     hours: number;
     rate: number;
     rate: number;
     total: number;
     total: number;
+    pricingType?: string;
     excluded?: boolean;
     excluded?: boolean;
   }[];
   }[];
   customer: {
   customer: {
@@ -580,7 +581,7 @@ export function QuoteView({
                       <thead>
                       <thead>
                         <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
                         <tr className="border-b text-left" style={{ backgroundColor: `${primaryColor}15` }}>
                           <th className="p-2 font-medium">{t('laborDescription')}</th>
                           <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('qtyOrHours')}</th>
                           <th className="p-2 text-right font-medium">{t('rate')}</th>
                           <th className="p-2 text-right font-medium">{t('rate')}</th>
                           <th className="p-2 text-right font-medium">{t('total')}</th>
                           <th className="p-2 text-right font-medium">{t('total')}</th>
                         </tr>
                         </tr>
@@ -589,8 +590,8 @@ export function QuoteView({
                         {quote.laborItems.map((l, i) => (
                         {quote.laborItems.map((l, i) => (
                           <tr key={i} className={l.excluded ? "line-through text-gray-400" : ""}>
                           <tr key={i} className={l.excluded ? "line-through text-gray-400" : ""}>
                             <td className="p-2">{l.description}</td>
                             <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">{l.pricingType === 'service' ? `${l.hours} ${t('unit')}` : `${l.hours} ${t('hrs')}`}</td>
+                            <td className="p-2 text-right">{l.pricingType === 'service' ? formatCurrency(l.rate, currencyCode) : t('ratePerHour', { rate: formatCurrency(l.rate, currencyCode) })}</td>
                             <td className="p-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
                             <td className="p-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
                           </tr>
                           </tr>
                         ))}
                         ))}

+ 4 - 0
src/features/labor-presets/Actions/laborPresetActions.ts

@@ -50,6 +50,7 @@ export async function getLaborPresetsPaginated(params: {
         take: pageSize,
         take: pageSize,
         include: {
         include: {
           _count: { select: { items: true } },
           _count: { select: { items: true } },
+          items: { select: { hours: true, pricingType: true } },
         },
         },
       }),
       }),
       db.laborPreset.count({ where }),
       db.laborPreset.count({ where }),
@@ -93,6 +94,7 @@ export async function createLaborPreset(input: unknown) {
             description: item.description,
             description: item.description,
             hours: item.hours,
             hours: item.hours,
             rate: item.rate,
             rate: item.rate,
+            pricingType: item.pricingType || "hourly",
             sortOrder: index,
             sortOrder: index,
           })),
           })),
         },
         },
@@ -137,6 +139,7 @@ export async function updateLaborPreset(input: unknown) {
               description: item.description,
               description: item.description,
               hours: item.hours,
               hours: item.hours,
               rate: item.rate,
               rate: item.rate,
+              pricingType: item.pricingType || "hourly",
               sortOrder: index,
               sortOrder: index,
             })),
             })),
           } : undefined,
           } : undefined,
@@ -192,6 +195,7 @@ export async function getLaborPresetsList() {
             description: true,
             description: true,
             hours: true,
             hours: true,
             rate: true,
             rate: true,
+            pricingType: true,
             sortOrder: true,
             sortOrder: true,
           },
           },
           orderBy: { sortOrder: "asc" },
           orderBy: { sortOrder: "asc" },

+ 68 - 29
src/features/labor-presets/Components/LaborPresetForm.tsx

@@ -17,10 +17,13 @@ import { useGlassModal } from "@/components/glass-modal";
 import { createLaborPreset, updateLaborPreset } from "../Actions/laborPresetActions";
 import { createLaborPreset, updateLaborPreset } from "../Actions/laborPresetActions";
 import { Loader2, Plus, Trash2 } from "lucide-react";
 import { Loader2, Plus, Trash2 } from "lucide-react";
 
 
+type PricingType = "hourly" | "service";
+
 interface PresetItem {
 interface PresetItem {
   description: string;
   description: string;
   hours: number;
   hours: number;
   rate: number;
   rate: number;
+  pricingType: PricingType;
 }
 }
 
 
 interface SingleItem {
 interface SingleItem {
@@ -28,13 +31,14 @@ interface SingleItem {
   description: string;
   description: string;
   hours: number;
   hours: number;
   rate: number;
   rate: number;
+  pricingType: PricingType;
 }
 }
 
 
 interface LaborPresetData {
 interface LaborPresetData {
   id: string;
   id: string;
   name: string;
   name: string;
   description: string | null;
   description: string | null;
-  items: { description: string; hours: number; rate: number; sortOrder: number }[];
+  items: { description: string; hours: number; rate: number; pricingType?: string; sortOrder: number }[];
 }
 }
 
 
 type PresetMode = "single" | "package";
 type PresetMode = "single" | "package";
@@ -52,6 +56,22 @@ function detectMode(preset?: LaborPresetData): PresetMode {
   return "package";
   return "package";
 }
 }
 
 
+function PricingTypeToggle({ value, onChange, tHourly, tService }: { value: PricingType; onChange: (v: PricingType) => void; tHourly: string; tService: string }) {
+  return (
+    <button
+      type="button"
+      className={`shrink-0 rounded-md border px-2 text-[10px] font-medium transition-colors ${
+        value === "service"
+          ? "border-blue-500/30 bg-blue-500/10 text-blue-600"
+          : "border-muted text-muted-foreground hover:text-foreground"
+      }`}
+      onClick={() => onChange(value === "service" ? "hourly" : "service")}
+    >
+      {value === "service" ? tService : tHourly}
+    </button>
+  );
+}
+
 export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate = 0 }: LaborPresetFormProps) {
 export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate = 0 }: LaborPresetFormProps) {
   const router = useRouter();
   const router = useRouter();
   const modal = useGlassModal();
   const modal = useGlassModal();
@@ -63,17 +83,17 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
   const [name, setName] = useState(preset?.name ?? "");
   const [name, setName] = useState(preset?.name ?? "");
   const [description, setDescription] = useState(preset?.description ?? "");
   const [description, setDescription] = useState(preset?.description ?? "");
   const [items, setItems] = useState<PresetItem[]>(
   const [items, setItems] = useState<PresetItem[]>(
-    preset?.items.map((i) => ({ description: i.description, hours: i.hours, rate: i.rate })) ?? [
-      { description: "", hours: 0, rate: defaultLaborRate },
+    preset?.items.map((i) => ({ description: i.description, hours: i.hours, rate: i.rate, pricingType: (i.pricingType as PricingType) || "hourly" })) ?? [
+      { description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" },
     ]
     ]
   );
   );
 
 
   // Single item mode state
   // Single item mode state
   const [singleItems, setSingleItems] = useState<SingleItem[]>(() => {
   const [singleItems, setSingleItems] = useState<SingleItem[]>(() => {
     if (preset && detectMode(preset) === "single") {
     if (preset && detectMode(preset) === "single") {
-      return [{ name: preset.items[0].description, description: preset.description ?? "", hours: preset.items[0].hours, rate: preset.items[0].rate }];
+      return [{ name: preset.items[0].description, description: preset.description ?? "", hours: preset.items[0].hours, rate: preset.items[0].rate, pricingType: (preset.items[0].pricingType as PricingType) || "hourly" }];
     }
     }
-    return [{ name: "", description: "", hours: 0, rate: defaultLaborRate }];
+    return [{ name: "", description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" }];
   });
   });
 
 
   const handleOpenChange = (isOpen: boolean) => {
   const handleOpenChange = (isOpen: boolean) => {
@@ -83,14 +103,14 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
       setName(preset?.name ?? "");
       setName(preset?.name ?? "");
       setDescription(preset?.description ?? "");
       setDescription(preset?.description ?? "");
       setItems(
       setItems(
-        preset?.items.map((i) => ({ description: i.description, hours: i.hours, rate: i.rate })) ?? [
-          { description: "", hours: 0, rate: defaultLaborRate },
+        preset?.items.map((i) => ({ description: i.description, hours: i.hours, rate: i.rate, pricingType: (i.pricingType as PricingType) || "hourly" })) ?? [
+          { description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" },
         ]
         ]
       );
       );
       if (detectedMode === "single" && preset) {
       if (detectedMode === "single" && preset) {
-        setSingleItems([{ name: preset.items[0].description, description: preset.description ?? "", hours: preset.items[0].hours, rate: preset.items[0].rate }]);
+        setSingleItems([{ name: preset.items[0].description, description: preset.description ?? "", hours: preset.items[0].hours, rate: preset.items[0].rate, pricingType: (preset.items[0].pricingType as PricingType) || "hourly" }]);
       } else {
       } else {
-        setSingleItems([{ name: "", description: "", hours: 0, rate: defaultLaborRate }]);
+        setSingleItems([{ name: "", description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" }]);
       }
       }
     }
     }
     onOpenChange(isOpen);
     onOpenChange(isOpen);
@@ -110,7 +130,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
   };
   };
 
 
   const addItem = () => {
   const addItem = () => {
-    setItems((prev) => [...prev, { description: "", hours: 0, rate: defaultLaborRate }]);
+    setItems((prev) => [...prev, { description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" }]);
   };
   };
 
 
   const removeItem = (index: number) => {
   const removeItem = (index: number) => {
@@ -127,7 +147,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
   };
   };
 
 
   const addSingleItem = () => {
   const addSingleItem = () => {
-    setSingleItems((prev) => [...prev, { name: "", description: "", hours: 0, rate: defaultLaborRate }]);
+    setSingleItems((prev) => [...prev, { name: "", description: "", hours: 0, rate: defaultLaborRate, pricingType: "hourly" }]);
   };
   };
 
 
   const removeSingleItem = (index: number) => {
   const removeSingleItem = (index: number) => {
@@ -153,7 +173,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
           id: preset.id,
           id: preset.id,
           name: "",
           name: "",
           description: item.description || undefined,
           description: item.description || undefined,
-          items: [{ description: item.name, hours: Number(item.hours) || 0, rate: Number(item.rate) || 0, sortOrder: 0 }],
+          items: [{ description: item.name, hours: Number(item.hours) || 0, rate: Number(item.rate) || 0, pricingType: item.pricingType, sortOrder: 0 }],
         });
         });
         if (result.success) {
         if (result.success) {
           onOpenChange(false);
           onOpenChange(false);
@@ -168,7 +188,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
           const result = await createLaborPreset({
           const result = await createLaborPreset({
             name: "",
             name: "",
             description: item.description || undefined,
             description: item.description || undefined,
-            items: [{ description: item.name, hours: Number(item.hours) || 0, rate: Number(item.rate) || 0, sortOrder: 0 }],
+            items: [{ description: item.name, hours: Number(item.hours) || 0, rate: Number(item.rate) || 0, pricingType: item.pricingType, sortOrder: 0 }],
           });
           });
           if (!result.success) {
           if (!result.success) {
             modal.open("error", t("errors.error"), result.error || t("errors.saveFailed"));
             modal.open("error", t("errors.error"), result.error || t("errors.saveFailed"));
@@ -197,6 +217,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
           description: item.description,
           description: item.description,
           hours: Number(item.hours) || 0,
           hours: Number(item.hours) || 0,
           rate: Number(item.rate) || 0,
           rate: Number(item.rate) || 0,
+          pricingType: item.pricingType,
           sortOrder: index,
           sortOrder: index,
         })),
         })),
       };
       };
@@ -256,7 +277,7 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
             <div className="space-y-2">
             <div className="space-y-2">
               <div className="hidden grid-cols-[2fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
               <div className="hidden grid-cols-[2fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
                 <span>{t("form.singleNameLabel")}</span>
                 <span>{t("form.singleNameLabel")}</span>
-                <span>{t("form.itemHours")}</span>
+                <span>{t("form.itemQtyOrHours")}</span>
                 <span>{t("form.itemRate")}</span>
                 <span>{t("form.itemRate")}</span>
                 <span />
                 <span />
               </div>
               </div>
@@ -264,16 +285,25 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
               {singleItems.map((item, i) => (
               {singleItems.map((item, i) => (
                 <div key={i} className="space-y-2">
                 <div key={i} className="space-y-2">
                   <div className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_auto]">
                   <div className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_auto]">
-                    <Input
-                      placeholder={t("form.singleNamePlaceholder")}
-                      value={item.name}
-                      onChange={(e) => updateSingleItem(i, "name", e.target.value)}
-                      className="col-span-2 sm:col-span-1"
-                    />
+                    <div className="col-span-2 flex gap-2 sm:col-span-1">
+                      <Input
+                        placeholder={t("form.singleNamePlaceholder")}
+                        value={item.name}
+                        onChange={(e) => updateSingleItem(i, "name", e.target.value)}
+                        className="flex-1"
+                      />
+                      <PricingTypeToggle
+                        value={item.pricingType}
+                        onChange={(v) => updateSingleItem(i, "pricingType", v)}
+                        tHourly={t("form.hourlyTag")}
+                        tService={t("form.serviceTag")}
+                      />
+                    </div>
                     <Input
                     <Input
                       type="number"
                       type="number"
                       min="0"
                       min="0"
-                      step="any"
+                      step={item.pricingType === "service" ? "1" : "any"}
+                      placeholder={item.pricingType === "service" ? t("form.itemQty") : t("form.itemHours")}
                       value={item.hours}
                       value={item.hours}
                       onChange={(e) => updateSingleItem(i, "hours", e.target.value)}
                       onChange={(e) => updateSingleItem(i, "hours", e.target.value)}
                     />
                     />
@@ -347,23 +377,32 @@ export function LaborPresetForm({ open, onOpenChange, preset, defaultLaborRate =
 
 
                 <div className="hidden grid-cols-[2fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
                 <div className="hidden grid-cols-[2fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
                   <span>{t("form.itemDescription")}</span>
                   <span>{t("form.itemDescription")}</span>
-                  <span>{t("form.itemHours")}</span>
+                  <span>{t("form.itemQtyOrHours")}</span>
                   <span>{t("form.itemRate")}</span>
                   <span>{t("form.itemRate")}</span>
                   <span />
                   <span />
                 </div>
                 </div>
 
 
                 {items.map((item, i) => (
                 {items.map((item, i) => (
                   <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_auto]">
                   <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_auto]">
-                    <Input
-                      placeholder={t("form.itemDescriptionPlaceholder")}
-                      value={item.description}
-                      onChange={(e) => updateItem(i, "description", e.target.value)}
-                      className="col-span-2 sm:col-span-1"
-                    />
+                    <div className="col-span-2 flex gap-2 sm:col-span-1">
+                      <Input
+                        placeholder={t("form.itemDescriptionPlaceholder")}
+                        value={item.description}
+                        onChange={(e) => updateItem(i, "description", e.target.value)}
+                        className="flex-1"
+                      />
+                      <PricingTypeToggle
+                        value={item.pricingType}
+                        onChange={(v) => updateItem(i, "pricingType", v)}
+                        tHourly={t("form.hourlyTag")}
+                        tService={t("form.serviceTag")}
+                      />
+                    </div>
                     <Input
                     <Input
                       type="number"
                       type="number"
                       min="0"
                       min="0"
-                      step="any"
+                      step={item.pricingType === "service" ? "1" : "any"}
+                      placeholder={item.pricingType === "service" ? t("form.itemQty") : t("form.itemHours")}
                       value={item.hours}
                       value={item.hours}
                       onChange={(e) => updateItem(i, "hours", e.target.value)}
                       onChange={(e) => updateItem(i, "hours", e.target.value)}
                     />
                     />

+ 20 - 8
src/features/labor-presets/Components/LaborPresetPickerDialog.tsx

@@ -10,7 +10,7 @@ export interface LaborPresetOption {
   id: string;
   id: string;
   name: string;
   name: string;
   description: string | null;
   description: string | null;
-  items: { description: string; hours: number; rate: number; sortOrder: number }[];
+  items: { description: string; hours: number; rate: number; pricingType?: string; sortOrder: number }[];
 }
 }
 
 
 interface LaborPresetPickerDialogProps {
 interface LaborPresetPickerDialogProps {
@@ -65,19 +65,30 @@ export function LaborPresetPickerDialog({
         </div>
         </div>
         <div className="max-h-75 overflow-y-auto space-y-1">
         <div className="max-h-75 overflow-y-auto space-y-1">
           {filtered.map((preset) => {
           {filtered.map((preset) => {
-            const totalHours = preset.items.reduce((sum, i) => sum + i.hours, 0);
+            const hourlyItems = preset.items.filter((i) => i.pricingType !== "service");
+            const serviceItems = preset.items.filter((i) => i.pricingType === "service");
+            const totalHours = hourlyItems.reduce((sum, i) => sum + i.hours, 0);
+            const totalUnits = serviceItems.reduce((sum, i) => sum + i.hours, 0);
             const isExpanded = expandedId === preset.id;
             const isExpanded = expandedId === preset.id;
             const isSingleItem = preset.items.length === 1;
             const isSingleItem = preset.items.length === 1;
 
 
             return (
             return (
               <div key={preset.id} className="rounded-md border border-transparent hover:border-border">
               <div key={preset.id} className="rounded-md border border-transparent hover:border-border">
-                <button
-                  type="button"
-                  className="w-full text-left rounded-md px-2.5 py-2 hover:bg-accent transition-colors flex items-center justify-between gap-4"
+                <div
+                  role="button"
+                  tabIndex={0}
+                  className="w-full text-left rounded-md px-2.5 py-2 hover:bg-accent transition-colors flex items-center justify-between gap-4 cursor-pointer"
                   onClick={() => {
                   onClick={() => {
                     onSelectPreset(preset);
                     onSelectPreset(preset);
                     onOpenChange(false);
                     onOpenChange(false);
                   }}
                   }}
+                  onKeyDown={(e) => {
+                    if (e.key === "Enter" || e.key === " ") {
+                      e.preventDefault();
+                      onSelectPreset(preset);
+                      onOpenChange(false);
+                    }
+                  }}
                 >
                 >
                   <div className="min-w-0 flex-1">
                   <div className="min-w-0 flex-1">
                     <div className="flex items-center gap-2">
                     <div className="flex items-center gap-2">
@@ -89,7 +100,8 @@ export function LaborPresetPickerDialog({
                   </div>
                   </div>
                   <div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
                   <div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
                     {!isSingleItem && <span>{t("items", { count: preset.items.length })}</span>}
                     {!isSingleItem && <span>{t("items", { count: preset.items.length })}</span>}
-                    <span>{t("totalHours", { hours: totalHours.toFixed(1) })}</span>
+                    {totalHours > 0 && <span>{t("totalHours", { hours: totalHours.toFixed(1) })}</span>}
+                    {totalUnits > 0 && <span>{t("totalUnits", { units: totalUnits })}</span>}
                     {!isSingleItem && (
                     {!isSingleItem && (
                       <button
                       <button
                         type="button"
                         type="button"
@@ -103,13 +115,13 @@ export function LaborPresetPickerDialog({
                       </button>
                       </button>
                     )}
                     )}
                   </div>
                   </div>
-                </button>
+                </div>
                 {isExpanded && !isSingleItem && (
                 {isExpanded && !isSingleItem && (
                   <div className="px-3 pb-2 space-y-0.5">
                   <div className="px-3 pb-2 space-y-0.5">
                     {preset.items.map((item, i) => (
                     {preset.items.map((item, i) => (
                       <div key={i} className="flex items-center justify-between text-xs text-muted-foreground py-0.5 pl-2 border-l-2 border-muted">
                       <div key={i} className="flex items-center justify-between text-xs text-muted-foreground py-0.5 pl-2 border-l-2 border-muted">
                         <span className="truncate">{item.description}</span>
                         <span className="truncate">{item.description}</span>
-                        <span className="shrink-0 ml-2">{item.hours}h</span>
+                        <span className="shrink-0 ml-2">{item.pricingType === "service" ? `${item.hours} unit${item.hours !== 1 ? "s" : ""}` : `${item.hours}h`}</span>
                       </div>
                       </div>
                     ))}
                     ))}
                   </div>
                   </div>

+ 1 - 0
src/features/labor-presets/Schema/laborPresetSchema.ts

@@ -4,6 +4,7 @@ export const laborPresetItemSchema = z.object({
   description: z.string().min(1, "Description is required"),
   description: z.string().min(1, "Description is required"),
   hours: z.coerce.number().min(0, "Hours must be 0 or more").default(0),
   hours: z.coerce.number().min(0, "Hours must be 0 or more").default(0),
   rate: z.coerce.number().min(0, "Rate must be 0 or more").default(0),
   rate: z.coerce.number().min(0, "Rate must be 0 or more").default(0),
+  pricingType: z.enum(["hourly", "service"]).default("hourly"),
   sortOrder: z.coerce.number().int().min(0).default(0),
   sortOrder: z.coerce.number().int().min(0).default(0),
 });
 });
 
 

+ 1 - 0
src/features/quotes/Actions/quoteActions.ts

@@ -368,6 +368,7 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
             hours: l.hours,
             hours: l.hours,
             rate: l.rate,
             rate: l.rate,
             total: l.total,
             total: l.total,
+            pricingType: l.pricingType || "hourly",
             serviceRecordId: created.id,
             serviceRecordId: created.id,
           })),
           })),
         });
         });

+ 52 - 7
src/features/quotes/Components/QuoteLaborEditor.tsx

@@ -3,7 +3,7 @@
 import { memo } from "react";
 import { memo } from "react";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
-import { Layers, Plus, Trash2 } from "lucide-react";
+import { Layers, Plus, Trash2, Wrench } from "lucide-react";
 import { formatCurrency } from "@/lib/format";
 import { formatCurrency } from "@/lib/format";
 import type { QuoteLaborInput } from "./quote-page-types";
 import type { QuoteLaborInput } from "./quote-page-types";
 
 
@@ -14,6 +14,12 @@ const QuoteLaborRow = memo(function QuoteLaborRow({
   onUpdate,
   onUpdate,
   onDelete,
   onDelete,
   tDescriptionPlaceholder,
   tDescriptionPlaceholder,
+  tHourlyTag,
+  tServiceTag,
+  tSwitchToHourly,
+  tSwitchToService,
+  tQty,
+  tHours,
 }: {
 }: {
   labor: QuoteLaborInput;
   labor: QuoteLaborInput;
   index: number;
   index: number;
@@ -21,11 +27,32 @@ const QuoteLaborRow = memo(function QuoteLaborRow({
   onUpdate: (index: number, field: keyof QuoteLaborInput, value: string | number | boolean) => void;
   onUpdate: (index: number, field: keyof QuoteLaborInput, value: string | number | boolean) => void;
   onDelete: (index: number) => void;
   onDelete: (index: number) => void;
   tDescriptionPlaceholder: string;
   tDescriptionPlaceholder: string;
+  tHourlyTag: string;
+  tServiceTag: string;
+  tSwitchToHourly: string;
+  tSwitchToService: string;
+  tQty: string;
+  tHours: string;
 }) {
 }) {
+  const isService = labor.pricingType === "service";
   return (
   return (
     <div className={`grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]${labor.excluded ? " line-through opacity-50" : ""}`}>
     <div className={`grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]${labor.excluded ? " line-through opacity-50" : ""}`}>
-      <Input placeholder={tDescriptionPlaceholder} value={labor.description} onChange={(e) => onUpdate(index, "description", e.target.value)} className="col-span-2 sm:col-span-1" />
-      <Input type="number" min="0" step="0.1" value={labor.hours} onChange={(e) => onUpdate(index, "hours", e.target.value)} />
+      <div className="col-span-2 flex gap-2 sm:col-span-1">
+        <Input placeholder={tDescriptionPlaceholder} value={labor.description} onChange={(e) => onUpdate(index, "description", e.target.value)} className="flex-1" />
+        <button
+          type="button"
+          className={`shrink-0 rounded-md border px-2 text-[10px] font-medium transition-all ${
+            isService
+              ? "border-blue-500/30 bg-blue-500/10 text-blue-600 hover:bg-blue-500/20 hover:border-blue-500/50"
+              : "border-muted text-muted-foreground hover:bg-muted hover:text-foreground hover:border-foreground/20"
+          }`}
+          onClick={() => onUpdate(index, "pricingType", isService ? "hourly" : "service")}
+          title={isService ? tSwitchToHourly : tSwitchToService}
+        >
+          {isService ? tServiceTag : tHourlyTag}
+        </button>
+      </div>
+      <Input type="number" min="0" step={isService ? "1" : "0.1"} placeholder={isService ? tQty : tHours} value={labor.hours} onChange={(e) => onUpdate(index, "hours", e.target.value)} />
       <Input type="number" min="0" step="0.01" value={labor.rate} onChange={(e) => onUpdate(index, "rate", e.target.value)} />
       <Input type="number" min="0" step="0.01" value={labor.rate} onChange={(e) => onUpdate(index, "rate", e.target.value)} />
       <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(labor.total, currencyCode)}</div>
       <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(labor.total, currencyCode)}</div>
       <div className="flex items-center gap-1">
       <div className="flex items-center gap-1">
@@ -44,6 +71,7 @@ interface QuoteLaborEditorProps {
   onUpdate: (index: number, field: keyof QuoteLaborInput, value: string | number | boolean) => void;
   onUpdate: (index: number, field: keyof QuoteLaborInput, value: string | number | boolean) => void;
   onDelete: (index: number) => void;
   onDelete: (index: number) => void;
   onAdd: () => void;
   onAdd: () => void;
+  onAddService?: () => void;
   hasPresets?: boolean;
   hasPresets?: boolean;
   onOpenPresets?: () => void;
   onOpenPresets?: () => void;
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -58,6 +86,7 @@ export const QuoteLaborEditor = memo(function QuoteLaborEditor({
   onUpdate,
   onUpdate,
   onDelete,
   onDelete,
   onAdd,
   onAdd,
+  onAddService,
   hasPresets,
   hasPresets,
   onOpenPresets,
   onOpenPresets,
   t,
   t,
@@ -74,12 +103,15 @@ export const QuoteLaborEditor = memo(function QuoteLaborEditor({
             </Button>
             </Button>
           )}
           )}
           <Button type="button" variant="outline" size="sm" onClick={onAdd}><Plus className="mr-1 h-3.5 w-3.5" /> {t("labor.addLabor")}</Button>
           <Button type="button" variant="outline" size="sm" onClick={onAdd}><Plus className="mr-1 h-3.5 w-3.5" /> {t("labor.addLabor")}</Button>
+          {onAddService && (
+            <Button type="button" variant="outline" size="sm" onClick={onAddService}><Wrench className="mr-1 h-3.5 w-3.5" /> {t("labor.addService")}</Button>
+          )}
         </div>
         </div>
       </div>
       </div>
       {laborItems.length > 0 && (
       {laborItems.length > 0 && (
         <>
         <>
           <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
           <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
-            <span>{t("labor.description")}</span><span>{t("labor.hours")}</span><span>{t("labor.rate", { currency: cs })}</span><span>{t("labor.total")}</span><span />
+            <span>{t("labor.description")}</span><span>{t("labor.qtyOrHours")}</span><span>{t("labor.rate", { currency: cs })}</span><span>{t("labor.total")}</span><span />
           </div>
           </div>
           {laborItems.map((labor, i) => (
           {laborItems.map((labor, i) => (
             <QuoteLaborRow
             <QuoteLaborRow
@@ -90,6 +122,12 @@ export const QuoteLaborEditor = memo(function QuoteLaborEditor({
               onUpdate={onUpdate}
               onUpdate={onUpdate}
               onDelete={onDelete}
               onDelete={onDelete}
               tDescriptionPlaceholder={t("labor.descriptionPlaceholder")}
               tDescriptionPlaceholder={t("labor.descriptionPlaceholder")}
+              tHourlyTag={t("labor.hourlyTag")}
+              tServiceTag={t("labor.serviceTag")}
+              tSwitchToHourly={t("labor.switchToHourlyHint")}
+              tSwitchToService={t("labor.switchToServiceHint")}
+              tQty={t("labor.qty")}
+              tHours={t("labor.hours")}
             />
             />
           ))}
           ))}
           <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={onAdd}><Plus className="h-4 w-4" /></button>
           <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={onAdd}><Plus className="h-4 w-4" /></button>
@@ -97,9 +135,16 @@ export const QuoteLaborEditor = memo(function QuoteLaborEditor({
         </>
         </>
       )}
       )}
       {laborItems.length === 0 && (
       {laborItems.length === 0 && (
-        <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={onAdd}>
-          <Plus className="mr-1 h-4 w-4" /><span className="text-sm">{t("labor.addLabor")}</span>
-        </button>
+        <div className="flex gap-2">
+          <button type="button" className="flex flex-1 items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={onAdd}>
+            <Plus className="mr-1 h-4 w-4" /><span className="text-sm">{t("labor.addLabor")}</span>
+          </button>
+          {onAddService && (
+            <button type="button" className="flex flex-1 items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={onAddService}>
+              <Wrench className="mr-1 h-4 w-4" /><span className="text-sm">{t("labor.addService")}</span>
+            </button>
+          )}
+        </div>
       )}
       )}
     </div>
     </div>
   );
   );

+ 25 - 16
src/features/quotes/Components/QuotePDF.tsx

@@ -39,7 +39,7 @@ interface QuoteData {
     total: number
     total: number
     excluded?: boolean
     excluded?: boolean
   }[]
   }[]
-  laborItems: { description: string; hours: number; rate: number; total: number; excluded?: boolean }[]
+  laborItems: { description: string; hours: number; rate: number; total: number; pricingType?: string; excluded?: boolean }[]
   customer: {
   customer: {
     name: string
     name: string
     email: string | null
     email: string | null
@@ -471,7 +471,7 @@ export function QuotePDF({
           <View style={styles.tableHeader}>
           <View style={styles.tableHeader}>
             <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.description || 'Description'}</Text>
             <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.description || 'Description'}</Text>
             <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>
             <Text style={{ ...styles.tableHeaderCell, width: '15%', textAlign: 'right' }}>
-              {labels.hours || 'Hours'}
+              {labels.qtyOrHours || labels.hours || 'Qty / Hours'}
             </Text>
             </Text>
             <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
             <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>
               {labels.rate || 'Rate'}
               {labels.rate || 'Rate'}
@@ -480,20 +480,29 @@ export function QuotePDF({
               {labels.total || 'Total'}
               {labels.total || 'Total'}
             </Text>
             </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>
-          ))}
+          {data.laborItems.map((l, i) => {
+            const isService = l.pricingType === 'service'
+            return (
+              <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' } : {}) }}>
+                  {isService
+                    ? `${l.hours} ${labels.unit || 'unit'}`
+                    : `${l.hours} ${labels.hrs || 'hrs'}`}
+                </Text>
+                <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
+                  {isService
+                    ? formatCurrency(l.rate, currencyCode)
+                    : 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>
       </View>
       </View>
     )
     )

+ 4 - 2
src/features/quotes/Components/QuotePageClient.tsx

@@ -102,8 +102,9 @@ export function QuotePageClient({
       const newItems = preset.items.map((item) => ({
       const newItems = preset.items.map((item) => ({
         description: item.description,
         description: item.description,
         hours: item.hours,
         hours: item.hours,
-        rate: item.rate > 0 ? item.rate : defaultLaborRate,
-        total: item.hours * (item.rate > 0 ? item.rate : defaultLaborRate),
+        rate: item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate),
+        total: item.hours * (item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate)),
+        pricingType: (item.pricingType as "hourly" | "service") || "hourly",
         excluded: false,
         excluded: false,
       }))
       }))
       state.addLaborBulk(newItems)
       state.addLaborBulk(newItems)
@@ -155,6 +156,7 @@ export function QuotePageClient({
         onUpdate={state.updateLabor}
         onUpdate={state.updateLabor}
         onDelete={state.deleteLabor}
         onDelete={state.deleteLabor}
         onAdd={state.addLabor}
         onAdd={state.addLabor}
+        onAddService={state.addService}
         hasPresets={laborPresets.length > 0}
         hasPresets={laborPresets.length > 0}
         onOpenPresets={() => setShowPresetPicker(true)}
         onOpenPresets={() => setShowPresetPicker(true)}
         t={t}
         t={t}

+ 11 - 1
src/features/quotes/Components/quote-page-types.ts

@@ -40,7 +40,7 @@ export interface QuoteRecord {
   createdAt: Date;
   createdAt: Date;
   updatedAt: Date;
   updatedAt: Date;
   partItems: { id: string; partNumber: string | null; name: string; quantity: number; unitPrice: number; total: number; excluded?: boolean }[];
   partItems: { id: string; partNumber: string | null; name: string; quantity: number; unitPrice: number; total: number; excluded?: boolean }[];
-  laborItems: { id: string; description: string; hours: number; rate: number; total: number; excluded?: boolean }[];
+  laborItems: { id: string; description: string; hours: number; rate: number; total: number; pricingType?: string; excluded?: boolean }[];
   customer: { id: string; name: string; email: string | null; phone: string | null; address: string | null; company: string | null } | null;
   customer: { id: string; name: string; email: string | null; phone: string | null; address: string | null; company: string | null } | null;
   vehicle: { id: string; make: string; model: string; year: number; vin: string | null; licensePlate: string | null; mileage: number } | null;
   vehicle: { id: string; make: string; model: string; year: number; vin: string | null; licensePlate: string | null; mileage: number } | null;
 }
 }
@@ -69,5 +69,15 @@ export const makeEmptyLabor = (defaultRate: number): QuoteLaborInput => ({
   hours: 0,
   hours: 0,
   rate: defaultRate,
   rate: defaultRate,
   total: 0,
   total: 0,
+  pricingType: "hourly",
+  excluded: false,
+});
+
+export const makeEmptyService = (): QuoteLaborInput => ({
+  description: "",
+  hours: 1,
+  rate: 0,
+  total: 0,
+  pricingType: "service",
   excluded: false,
   excluded: false,
 });
 });

+ 8 - 3
src/features/quotes/Components/useQuoteFormState.ts

@@ -8,7 +8,7 @@ import { useConfirm } from "@/components/confirm-dialog";
 import { updateQuote, deleteQuote, convertQuoteToServiceRecord } from "@/features/quotes/Actions/quoteActions";
 import { updateQuote, deleteQuote, convertQuoteToServiceRecord } from "@/features/quotes/Actions/quoteActions";
 import { acknowledgeQuoteResponse } from "@/features/quotes/Actions/quoteResponseActions";
 import { acknowledgeQuoteResponse } from "@/features/quotes/Actions/quoteResponseActions";
 import type { QuoteRecord, QuotePartInput, QuoteLaborInput } from "./quote-page-types";
 import type { QuoteRecord, QuotePartInput, QuoteLaborInput } from "./quote-page-types";
-import { emptyPart, makeEmptyLabor } from "./quote-page-types";
+import { emptyPart, makeEmptyLabor, makeEmptyService } from "./quote-page-types";
 
 
 interface SelectedVehicle {
 interface SelectedVehicle {
   id: string;
   id: string;
@@ -59,7 +59,7 @@ export function useQuoteFormState({
     quote.partItems.map((p) => ({ partNumber: p.partNumber || "", name: p.name, quantity: p.quantity, unitPrice: p.unitPrice, total: p.total, excluded: p.excluded ?? false }))
     quote.partItems.map((p) => ({ partNumber: p.partNumber || "", name: p.name, quantity: p.quantity, unitPrice: p.unitPrice, total: p.total, excluded: p.excluded ?? false }))
   );
   );
   const [laborItems, setLaborItems] = useState<QuoteLaborInput[]>(
   const [laborItems, setLaborItems] = useState<QuoteLaborInput[]>(
-    quote.laborItems.map((l) => ({ description: l.description, hours: l.hours, rate: l.rate, total: l.total, excluded: l.excluded ?? false }))
+    quote.laborItems.map((l) => ({ description: l.description, hours: l.hours, rate: l.rate, total: l.total, pricingType: (l.pricingType as "hourly" | "service") || "hourly", excluded: l.excluded ?? false }))
   );
   );
   const [taxRate, setTaxRate] = useState(quote.taxRate ?? defaultTaxRate);
   const [taxRate, setTaxRate] = useState(quote.taxRate ?? defaultTaxRate);
   const [discountType, setDiscountType] = useState<string>(quote.discountType || "none");
   const [discountType, setDiscountType] = useState<string>(quote.discountType || "none");
@@ -177,6 +177,11 @@ export function useQuoteFormState({
     markDirty();
     markDirty();
   }, [defaultLaborRate, markDirty]);
   }, [defaultLaborRate, markDirty]);
 
 
+  const addService = useCallback(() => {
+    setLaborItems((prev) => [...prev, makeEmptyService()]);
+    markDirty();
+  }, [markDirty]);
+
   const addLaborBulk = useCallback((items: QuoteLaborInput[]) => {
   const addLaborBulk = useCallback((items: QuoteLaborInput[]) => {
     setLaborItems((prev) => [...prev, ...items]);
     setLaborItems((prev) => [...prev, ...items]);
     markDirty();
     markDirty();
@@ -325,7 +330,7 @@ export function useQuoteFormState({
     partsSubtotal, laborSubtotal, subtotal, discountAmount, taxAmount, totalAmount,
     partsSubtotal, laborSubtotal, subtotal, discountAmount, taxAmount, totalAmount,
     selectedVehicle, setSelectedVehicle, selectedCustomer, setSelectedCustomer,
     selectedVehicle, setSelectedVehicle, selectedCustomer, setSelectedCustomer,
     // Callbacks
     // Callbacks
-    markDirty, updatePart, updateLabor, addPart, addLabor, addLaborBulk, deletePart, deleteLabor,
+    markDirty, updatePart, updateLabor, addPart, addLabor, addService, addLaborBulk, deletePart, deleteLabor,
     handleSubmit, handleDelete, handleDownloadPDF,
     handleSubmit, handleDelete, handleDownloadPDF,
     handleConvert, handleResolveResponse, saveNow,
     handleConvert, handleResolveResponse, saveNow,
   };
   };

+ 1 - 0
src/features/quotes/Schema/quoteSchema.ts

@@ -14,6 +14,7 @@ export const quoteLaborSchema = z.object({
   hours: z.coerce.number().min(0).default(0),
   hours: z.coerce.number().min(0).default(0),
   rate: z.coerce.number().min(0).default(0),
   rate: z.coerce.number().min(0).default(0),
   total: z.coerce.number().min(0).default(0),
   total: z.coerce.number().min(0).default(0),
+  pricingType: z.enum(["hourly", "service"]).default("hourly"),
   excluded: z.boolean().optional().default(false),
   excluded: z.boolean().optional().default(false),
 });
 });
 
 

+ 24 - 15
src/features/vehicles/Components/invoice-pdf/Tables.tsx

@@ -62,24 +62,33 @@ export function LaborTable({ data, currencyCode, styles, labels }: TablesProps)
       <View style={styles.table}>
       <View style={styles.table}>
         <View style={styles.tableHeader}>
         <View style={styles.tableHeader}>
           <Text style={{ ...styles.tableHeaderCell, width: '45%' }}>{labels.description || 'Description'}</Text>
           <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: '15%', textAlign: 'right' }}>{labels.qtyOrHours || labels.hours || 'Qty / Hours'}</Text>
           <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>{labels.rate || 'Rate'}</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>
           <Text style={{ ...styles.tableHeaderCell, width: '20%', textAlign: 'right' }}>{labels.total || 'Total'}</Text>
         </View>
         </View>
-        {data.laborItems.map((labor, i) => (
-          <View key={i} style={styles.tableRow}>
-            <Text style={{ ...styles.tableCell, width: '45%' }}>{labor.description}</Text>
-            <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right' }}>
-              {labor.hours}
-            </Text>
-            <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right' }}>
-              {labels.ratePerHour ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(labor.rate, currencyCode) }) : `${formatCurrency(labor.rate, currencyCode)}/hr`}
-            </Text>
-            <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
-              {formatCurrency(labor.total, currencyCode)}
-            </Text>
-          </View>
-        ))}
+        {data.laborItems.map((labor, i) => {
+          const isService = labor.pricingType === 'service'
+          return (
+            <View key={i} style={styles.tableRow}>
+              <Text style={{ ...styles.tableCell, width: '45%' }}>{labor.description}</Text>
+              <Text style={{ ...styles.tableCell, width: '15%', textAlign: 'right' }}>
+                {isService
+                  ? `${labor.hours} ${labels.unit || 'unit'}`
+                  : `${labor.hours} ${labels.hrs || 'hrs'}`}
+              </Text>
+              <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right' }}>
+                {isService
+                  ? formatCurrency(labor.rate, currencyCode)
+                  : labels.ratePerHour
+                    ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(labor.rate, currencyCode) })
+                    : `${formatCurrency(labor.rate, currencyCode)}/hr`}
+              </Text>
+              <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
+                {formatCurrency(labor.total, currencyCode)}
+              </Text>
+            </View>
+          )
+        })}
       </View>
       </View>
     </View>
     </View>
   )
   )

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

@@ -42,6 +42,7 @@ export interface InvoiceData {
     hours: number
     hours: number
     rate: number
     rate: number
     total: number
     total: number
+    pricingType?: string
   }[]
   }[]
   customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>;
   customFields?: Array<{ fieldId: string; label: string; value: string; fieldType: string }>;
   vehicle: {
   vehicle: {

+ 87 - 50
src/features/vehicles/Components/service-edit/LaborEditor.tsx

@@ -3,11 +3,11 @@
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Textarea } from '@/components/ui/textarea'
 import { Textarea } from '@/components/ui/textarea'
-import { Layers, Plus, Trash2 } from 'lucide-react'
+import { Layers, Plus, Trash2, Wrench } from 'lucide-react'
 import { formatCurrency, getCurrencySymbol } from '@/lib/format'
 import { formatCurrency, getCurrencySymbol } from '@/lib/format'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import type { ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
-import { makeEmptyLabor } from './form-types'
+import { makeEmptyLabor, makeEmptyService } from './form-types'
 
 
 interface LaborEditorProps {
 interface LaborEditorProps {
   laborItems: ServiceLaborInput[]
   laborItems: ServiceLaborInput[]
@@ -53,6 +53,15 @@ export function LaborEditor({
             <Plus className="mr-1 h-3.5 w-3.5" />
             <Plus className="mr-1 h-3.5 w-3.5" />
             {t('addLabor')}
             {t('addLabor')}
           </Button>
           </Button>
+          <Button
+            type="button"
+            variant="outline"
+            size="sm"
+            onClick={() => setLaborItems((prev) => [...prev, makeEmptyService()])}
+          >
+            <Wrench className="mr-1 h-3.5 w-3.5" />
+            {t('addService')}
+          </Button>
         </div>
         </div>
       </div>
       </div>
 
 
@@ -60,51 +69,69 @@ export function LaborEditor({
         <>
         <>
           <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
           <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
             <span>{t('description')}</span>
             <span>{t('description')}</span>
-            <span>{t('hours')}</span>
+            <span>{t('qtyOrHours')}</span>
             <span>{t('rate', { currency: cs })}</span>
             <span>{t('rate', { currency: cs })}</span>
             <span>{t('total')}</span>
             <span>{t('total')}</span>
             <span />
             <span />
           </div>
           </div>
-          {laborItems.map((labor, i) => (
-            <div
-              key={i}
-              className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]"
-            >
-              <Textarea
-                placeholder={t('descriptionPlaceholder')}
-                value={labor.description}
-                onChange={(e) => updateLabor(i, 'description', e.target.value)}
-                rows={1}
-                className="col-span-2 min-h-9 resize-none sm:col-span-1"
-              />
-              <Input
-                type="number"
-                min="0"
-                step="any"
-                value={labor.hours}
-                onChange={(e) => updateLabor(i, 'hours', e.target.value)}
-              />
-              <Input
-                type="number"
-                min="0"
-                step="0.01"
-                value={labor.rate}
-                onChange={(e) => updateLabor(i, 'rate', e.target.value)}
-              />
-              <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">
-                {formatCurrency(labor.total, currencyCode)}
-              </div>
-              <Button
-                type="button"
-                variant="ghost"
-                size="icon"
-                className="h-9 w-9 text-muted-foreground hover:text-destructive"
-                onClick={() => setLaborItems((prev) => prev.filter((_, j) => j !== i))}
+          {laborItems.map((labor, i) => {
+            const isService = labor.pricingType === 'service'
+            return (
+              <div
+                key={i}
+                className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]"
               >
               >
-                <Trash2 className="h-4 w-4" />
-              </Button>
-            </div>
-          ))}
+                <div className="col-span-2 flex gap-2 sm:col-span-1">
+                  <Textarea
+                    placeholder={t('descriptionPlaceholder')}
+                    value={labor.description}
+                    onChange={(e) => updateLabor(i, 'description', e.target.value)}
+                    rows={1}
+                    className="min-h-9 flex-1 resize-none"
+                  />
+                  <button
+                    type="button"
+                    className={`shrink-0 rounded-md border px-2 text-[10px] font-medium transition-all ${
+                      isService
+                        ? 'border-blue-500/30 bg-blue-500/10 text-blue-600 hover:bg-blue-500/20 hover:border-blue-500/50'
+                        : 'border-muted text-muted-foreground hover:bg-muted hover:text-foreground hover:border-foreground/20'
+                    }`}
+                    onClick={() => updateLabor(i, 'pricingType', isService ? 'hourly' : 'service')}
+                    title={isService ? t('switchToHourlyHint') : t('switchToServiceHint')}
+                  >
+                    {isService ? t('serviceTag') : t('hourlyTag')}
+                  </button>
+                </div>
+                <Input
+                  type="number"
+                  min="0"
+                  step={isService ? '1' : 'any'}
+                  placeholder={isService ? t('qty') : t('hours')}
+                  value={labor.hours}
+                  onChange={(e) => updateLabor(i, 'hours', e.target.value)}
+                />
+                <Input
+                  type="number"
+                  min="0"
+                  step="0.01"
+                  value={labor.rate}
+                  onChange={(e) => updateLabor(i, 'rate', e.target.value)}
+                />
+                <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">
+                  {formatCurrency(labor.total, currencyCode)}
+                </div>
+                <Button
+                  type="button"
+                  variant="ghost"
+                  size="icon"
+                  className="h-9 w-9 text-muted-foreground hover:text-destructive"
+                  onClick={() => setLaborItems((prev) => prev.filter((_, j) => j !== i))}
+                >
+                  <Trash2 className="h-4 w-4" />
+                </Button>
+              </div>
+            )
+          })}
           <button
           <button
             type="button"
             type="button"
             className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
             className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
@@ -121,14 +148,24 @@ export function LaborEditor({
       )}
       )}
 
 
       {laborItems.length === 0 && (
       {laborItems.length === 0 && (
-        <button
-          type="button"
-          className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
-          onClick={() => setLaborItems((prev) => [...prev, makeEmptyLabor(defaultLaborRate)])}
-        >
-          <Plus className="mr-1 h-4 w-4" />
-          <span className="text-sm">{t('addLabor')}</span>
-        </button>
+        <div className="flex gap-2">
+          <button
+            type="button"
+            className="flex flex-1 items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+            onClick={() => setLaborItems((prev) => [...prev, makeEmptyLabor(defaultLaborRate)])}
+          >
+            <Plus className="mr-1 h-4 w-4" />
+            <span className="text-sm">{t('addLabor')}</span>
+          </button>
+          <button
+            type="button"
+            className="flex flex-1 items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+            onClick={() => setLaborItems((prev) => [...prev, makeEmptyService()])}
+          >
+            <Wrench className="mr-1 h-4 w-4" />
+            <span className="text-sm">{t('addService')}</span>
+          </button>
+        </div>
       )}
       )}
     </div>
     </div>
   )
   )

+ 9 - 0
src/features/vehicles/Components/service-edit/form-types.ts

@@ -80,4 +80,13 @@ export const makeEmptyLabor = (defaultRate: number): ServiceLaborInput => ({
   hours: 0,
   hours: 0,
   rate: defaultRate,
   rate: defaultRate,
   total: 0,
   total: 0,
+  pricingType: 'hourly',
+})
+
+export const makeEmptyService = (): ServiceLaborInput => ({
+  description: '',
+  hours: 1,
+  rate: 0,
+  total: 0,
+  pricingType: 'service',
 })
 })

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

@@ -91,8 +91,9 @@ export function ServicePageClient({
     const newItems = preset.items.map((item) => ({
     const newItems = preset.items.map((item) => ({
       description: item.description,
       description: item.description,
       hours: item.hours,
       hours: item.hours,
-      rate: item.rate > 0 ? item.rate : defaultLaborRate,
-      total: item.hours * (item.rate > 0 ? item.rate : defaultLaborRate),
+      rate: item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate),
+      total: item.hours * (item.rate > 0 ? item.rate : (item.pricingType === 'service' ? 0 : defaultLaborRate)),
+      pricingType: (item.pricingType as 'hourly' | 'service') || 'hourly',
     }))
     }))
     formState.dirtySetLaborItems((prev) => [...prev, ...newItems])
     formState.dirtySetLaborItems((prev) => [...prev, ...newItems])
   }
   }

+ 1 - 0
src/features/vehicles/Schema/serviceSchema.ts

@@ -15,6 +15,7 @@ export const serviceLaborSchema = z.object({
   hours: z.coerce.number().min(0).default(0),
   hours: z.coerce.number().min(0).default(0),
   rate: z.coerce.number().min(0).default(0),
   rate: z.coerce.number().min(0).default(0),
   total: z.coerce.number().min(0).default(0),
   total: z.coerce.number().min(0).default(0),
+  pricingType: z.enum(["hourly", "service"]).default("hourly"),
 });
 });
 
 
 export const serviceAttachmentSchema = z.object({
 export const serviceAttachmentSchema = z.object({

+ 1 - 1
src/lib/cron/recurring-invoices.ts

@@ -107,7 +107,7 @@ export function processRecurringInvoices() {
                 },
                 },
                 laborItems: {
                 laborItems: {
                   create: ri.templateLabor.map((l) => ({
                   create: ri.templateLabor.map((l) => ({
-                    description: l.description, hours: l.hours, rate: l.rate, total: l.hours * l.rate,
+                    description: l.description, hours: l.hours, rate: l.rate, total: l.hours * l.rate, pricingType: l.pricingType || "hourly",
                   })),
                   })),
                 },
                 },
               },
               },