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

Fix shared invoice checkout and translate payment terms days (#162)

* added lkr currency

* add currency symbol selector

* Fix shared invoice payment checkout and translate Net days

- Server-side checkout balance now mirrors client by including
  partItems/laborItems and computing total via calculateTotals,
  so invoices without a stored totalAmount no longer reject with
  "Invoice is already paid in full" when pressing Pay with card.
- Replace hard-coded "Net X Days" string with a translated
  netDays key across all 12 locales (share.json + pdf.json),
  using the simpler "{days} <day-word>" form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bernt Christian Egeland 4 месяцев назад
Родитель
Сommit
7a1091be94
61 измененных файлов с 858 добавлено и 236 удалено
  1. 1 0
      messages/de/pdf.json
  2. 29 2
      messages/de/settings.json
  3. 1 0
      messages/de/share.json
  4. 1 0
      messages/en/pdf.json
  5. 29 2
      messages/en/settings.json
  6. 1 0
      messages/en/share.json
  7. 1 0
      messages/es/pdf.json
  8. 29 2
      messages/es/settings.json
  9. 1 0
      messages/es/share.json
  10. 1 0
      messages/fr/pdf.json
  11. 29 2
      messages/fr/settings.json
  12. 1 0
      messages/fr/share.json
  13. 1 0
      messages/it/pdf.json
  14. 29 2
      messages/it/settings.json
  15. 1 0
      messages/it/share.json
  16. 1 0
      messages/lt/pdf.json
  17. 29 2
      messages/lt/settings.json
  18. 1 0
      messages/lt/share.json
  19. 1 0
      messages/nb/pdf.json
  20. 29 2
      messages/nb/settings.json
  21. 1 0
      messages/nb/share.json
  22. 1 0
      messages/nl/pdf.json
  23. 29 2
      messages/nl/settings.json
  24. 1 0
      messages/nl/share.json
  25. 1 0
      messages/pl/pdf.json
  26. 29 2
      messages/pl/settings.json
  27. 1 0
      messages/pl/share.json
  28. 1 0
      messages/pt-BR/pdf.json
  29. 29 2
      messages/pt-BR/settings.json
  30. 1 0
      messages/pt-BR/share.json
  31. 1 0
      messages/ru/pdf.json
  32. 29 2
      messages/ru/settings.json
  33. 1 0
      messages/ru/share.json
  34. 1 0
      messages/tr/pdf.json
  35. 29 2
      messages/tr/settings.json
  36. 1 0
      messages/tr/share.json
  37. 6 0
      src/app/(authenticated)/layout.tsx
  38. 60 2
      src/app/(authenticated)/settings/localization/localization-settings.tsx
  39. 18 16
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  40. 3 0
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  41. 3 0
      src/app/(public)/share/quote/[orgId]/[token]/page.tsx
  42. 13 11
      src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx
  43. 1 0
      src/app/api/protected/quotes/[id]/pdf/route.ts
  44. 1 0
      src/app/api/protected/services/[id]/pdf/route.ts
  45. 1 0
      src/app/api/protected/vehicles/[id]/service-history-pdf/route.ts
  46. 25 2
      src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts
  47. 1 0
      src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts
  48. 63 0
      src/components/currency-settings-context.tsx
  49. 5 1
      src/features/email/Actions/emailActions.ts
  50. 14 12
      src/features/quotes/Components/QuotePDF.tsx
  51. 3 1
      src/features/reports/Components/ReportPDF.tsx
  52. 1 0
      src/features/settings/Schema/settingsSchema.ts
  53. 4 3
      src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx
  54. 1 1
      src/features/vehicles/Components/invoice-pdf/Notes.tsx
  55. 9 8
      src/features/vehicles/Components/invoice-pdf/Tables.tsx
  56. 11 9
      src/features/vehicles/Components/invoice-pdf/Totals.tsx
  57. 1 0
      src/features/vehicles/Components/invoice-pdf/types.ts
  58. 5 16
      src/features/vehicles/Components/service-history-pdf/ServiceHistoryPDF.tsx
  59. 3 1
      src/lib/cron/report-schedules.ts
  60. 228 129
      src/lib/format.ts
  61. 6 0
      src/lib/get-layout-data.ts

+ 1 - 0
messages/de/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Zahlungsinformationen",
     "orgNumberLabel": "Org.-Nr.",
     "paymentTermsLabel": "Zahlungsbedingungen",
+    "netDays": "{days} Tage",
     "dueDateLabel": "Fälligkeitsdatum",
     "warrantyTitle": "Garantie",
     "warrantyDuration": "Dauer",

+ 29 - 2
messages/de/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Philippinischer Peso",
       "IDR": "Indonesische Rupiah",
       "MYR": "Malaysischer Ringgit",
+      "LKR": "Sri-Lanka-Rupie",
       "AED": "VAE-Dirham",
       "SAR": "Saudi-Riyal",
-      "RUB": "Russischer Rubel"
-    }
+      "RUB": "Russischer Rubel",
+      "TWD": "Taiwan-Dollar",
+      "VND": "Vietnamesischer Dong",
+      "PKR": "Pakistanische Rupie",
+      "BDT": "Bangladesch-Taka",
+      "NPR": "Nepalesische Rupie",
+      "BGN": "Bulgarischer Lew",
+      "UAH": "Ukrainische Hrywnja",
+      "CLP": "Chilenischer Peso",
+      "COP": "Kolumbianischer Peso",
+      "ARS": "Argentinischer Peso",
+      "PEN": "Peruanischer Sol",
+      "EGP": "Ägyptisches Pfund",
+      "NGN": "Nigerianischer Naira",
+      "KES": "Kenia-Schilling",
+      "MAD": "Marokkanischer Dirham",
+      "GHS": "Ghanaischer Cedi",
+      "QAR": "Katar-Riyal",
+      "KWD": "Kuwait-Dinar",
+      "BHD": "Bahrain-Dinar",
+      "OMR": "Omanischer Rial",
+      "JOD": "Jordanischer Dinar"
+    },
+    "formatTitle": "Währungsformat",
+    "formatSymbolName": "Symbol",
+    "formatCodeName": "Code",
+    "formatSymbolLabel": "Symbol: {value}",
+    "formatCodeLabel": "Code: {value}"
   },
   "localization": {
     "languageTitle": "Sprache",

+ 1 - 0
messages/de/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Bankkonto",
     "orgNumber": "Org.-Nummer",
     "paymentTerms": "Zahlungsbedingungen",
+    "netDays": "{days} Tage",
     "dueDate": "Fälligkeitsdatum",
     "serviceImages": "Service-Bilder ({count})",
     "serviceVideos": "Service-Videos ({count})",

+ 1 - 0
messages/en/pdf.json

@@ -41,6 +41,7 @@
     "paymentInformation": "Payment Information",
     "orgNumberLabel": "Org. Number",
     "paymentTermsLabel": "Payment Terms",
+    "netDays": "{days} Days",
     "dueDateLabel": "Due Date",
     "warrantyTitle": "Warranty",
     "warrantyDuration": "Duration",

+ 29 - 2
messages/en/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Philippine Peso",
       "IDR": "Indonesian Rupiah",
       "MYR": "Malaysian Ringgit",
+      "LKR": "Sri Lankan Rupee",
       "AED": "UAE Dirham",
       "SAR": "Saudi Riyal",
-      "RUB": "Russian Ruble"
-    }
+      "RUB": "Russian Ruble",
+      "TWD": "Taiwan Dollar",
+      "VND": "Vietnamese Dong",
+      "PKR": "Pakistani Rupee",
+      "BDT": "Bangladeshi Taka",
+      "NPR": "Nepalese Rupee",
+      "BGN": "Bulgarian Lev",
+      "UAH": "Ukrainian Hryvnia",
+      "CLP": "Chilean Peso",
+      "COP": "Colombian Peso",
+      "ARS": "Argentine Peso",
+      "PEN": "Peruvian Sol",
+      "EGP": "Egyptian Pound",
+      "NGN": "Nigerian Naira",
+      "KES": "Kenyan Shilling",
+      "MAD": "Moroccan Dirham",
+      "GHS": "Ghanaian Cedi",
+      "QAR": "Qatari Riyal",
+      "KWD": "Kuwaiti Dinar",
+      "BHD": "Bahraini Dinar",
+      "OMR": "Omani Rial",
+      "JOD": "Jordanian Dinar"
+    },
+    "formatTitle": "Currency Format",
+    "formatSymbolName": "Symbol",
+    "formatCodeName": "Code",
+    "formatSymbolLabel": "Symbol: {value}",
+    "formatCodeLabel": "Code: {value}"
   },
   "localization": {
     "languageTitle": "Language",

+ 1 - 0
messages/en/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Bank Account",
     "orgNumber": "Org. Number",
     "paymentTerms": "Payment Terms",
+    "netDays": "{days} Days",
     "dueDate": "Due Date",
     "serviceImages": "Service Images ({count})",
     "serviceVideos": "Service Videos ({count})",

+ 1 - 0
messages/es/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Información de pago",
     "orgNumberLabel": "Núm. de org.",
     "paymentTermsLabel": "Condiciones de pago",
+    "netDays": "{days} días",
     "dueDateLabel": "Fecha de vencimiento",
     "warrantyTitle": "Garantía",
     "warrantyDuration": "Duración",

+ 29 - 2
messages/es/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Peso filipino",
       "IDR": "Rupia indonesia",
       "MYR": "Ringgit malayo",
+      "LKR": "Rupia de Sri Lanka",
       "AED": "Dirham de los EAU",
       "SAR": "Riyal saudi",
-      "RUB": "Rublo ruso"
-    }
+      "RUB": "Rublo ruso",
+      "TWD": "Dolar taiwanes",
+      "VND": "Dong vietnamita",
+      "PKR": "Rupia pakistani",
+      "BDT": "Taka de Bangladesh",
+      "NPR": "Rupia nepali",
+      "BGN": "Lev bulgaro",
+      "UAH": "Grivna ucraniana",
+      "CLP": "Peso chileno",
+      "COP": "Peso colombiano",
+      "ARS": "Peso argentino",
+      "PEN": "Sol peruano",
+      "EGP": "Libra egipcia",
+      "NGN": "Naira nigeriano",
+      "KES": "Chelin keniano",
+      "MAD": "Dirham marroqui",
+      "GHS": "Cedi ghanes",
+      "QAR": "Riyal qatari",
+      "KWD": "Dinar kuwaiti",
+      "BHD": "Dinar bahreini",
+      "OMR": "Rial omani",
+      "JOD": "Dinar jordano"
+    },
+    "formatTitle": "Formato de moneda",
+    "formatSymbolName": "Simbolo",
+    "formatCodeName": "Codigo",
+    "formatSymbolLabel": "Simbolo: {value}",
+    "formatCodeLabel": "Codigo: {value}"
   },
   "localization": {
     "languageTitle": "Idioma",

+ 1 - 0
messages/es/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Cuenta bancaria",
     "orgNumber": "N.º de organización",
     "paymentTerms": "Condiciones de pago",
+    "netDays": "{days} días",
     "dueDate": "Fecha de vencimiento",
     "serviceImages": "Imágenes del servicio ({count})",
     "serviceVideos": "Videos del servicio ({count})",

+ 1 - 0
messages/fr/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Informations de paiement",
     "orgNumberLabel": "N° d'org.",
     "paymentTermsLabel": "Conditions de paiement",
+    "netDays": "{days} jours",
     "dueDateLabel": "Date d'échéance",
     "warrantyTitle": "Garantie",
     "warrantyDuration": "Durée",

+ 29 - 2
messages/fr/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Peso philippin",
       "IDR": "Roupie indonesienne",
       "MYR": "Ringgit malaisien",
+      "LKR": "Roupie srilankaise",
       "AED": "Dirham des EAU",
       "SAR": "Riyal saoudien",
-      "RUB": "Rouble russe"
-    }
+      "RUB": "Rouble russe",
+      "TWD": "Dollar taiwanais",
+      "VND": "Dong vietnamien",
+      "PKR": "Roupie pakistanaise",
+      "BDT": "Taka bangladais",
+      "NPR": "Roupie nepalaise",
+      "BGN": "Lev bulgare",
+      "UAH": "Hryvnia ukrainienne",
+      "CLP": "Peso chilien",
+      "COP": "Peso colombien",
+      "ARS": "Peso argentin",
+      "PEN": "Sol peruvien",
+      "EGP": "Livre egyptienne",
+      "NGN": "Naira nigerian",
+      "KES": "Shilling kenyan",
+      "MAD": "Dirham marocain",
+      "GHS": "Cedi ghaneen",
+      "QAR": "Riyal qatari",
+      "KWD": "Dinar koweitien",
+      "BHD": "Dinar bahreini",
+      "OMR": "Rial omanais",
+      "JOD": "Dinar jordanien"
+    },
+    "formatTitle": "Format de la monnaie",
+    "formatSymbolName": "Symbole",
+    "formatCodeName": "Code",
+    "formatSymbolLabel": "Symbole: {value}",
+    "formatCodeLabel": "Code: {value}"
   },
   "localization": {
     "languageTitle": "Langue",

+ 1 - 0
messages/fr/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Compte bancaire",
     "orgNumber": "N° d'organisation",
     "paymentTerms": "Conditions de paiement",
+    "netDays": "{days} jours",
     "dueDate": "Date d'échéance",
     "serviceImages": "Images de service ({count})",
     "serviceVideos": "Vidéos de service ({count})",

+ 1 - 0
messages/it/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Informazioni di pagamento",
     "orgNumberLabel": "N. org.",
     "paymentTermsLabel": "Condizioni di pagamento",
+    "netDays": "{days} giorni",
     "dueDateLabel": "Data di scadenza",
     "warrantyTitle": "Garanzia",
     "warrantyDuration": "Durata",

+ 29 - 2
messages/it/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Peso filippino",
       "IDR": "Rupia indonesiana",
       "MYR": "Ringgit malese",
+      "LKR": "Rupia dello Sri Lanka",
       "AED": "Dirham degli EAU",
       "SAR": "Riyal saudita",
-      "RUB": "Rublo russo"
-    }
+      "RUB": "Rublo russo",
+      "TWD": "Dollaro taiwanese",
+      "VND": "Dong vietnamita",
+      "PKR": "Rupia pakistana",
+      "BDT": "Taka del Bangladesh",
+      "NPR": "Rupia nepalese",
+      "BGN": "Lev bulgaro",
+      "UAH": "Grivnia ucraina",
+      "CLP": "Peso cileno",
+      "COP": "Peso colombiano",
+      "ARS": "Peso argentino",
+      "PEN": "Sol peruviano",
+      "EGP": "Sterlina egiziana",
+      "NGN": "Naira nigeriana",
+      "KES": "Scellino keniota",
+      "MAD": "Dirham marocchino",
+      "GHS": "Cedi ghanese",
+      "QAR": "Riyal del Qatar",
+      "KWD": "Dinaro kuwaitiano",
+      "BHD": "Dinaro del Bahrein",
+      "OMR": "Rial omanita",
+      "JOD": "Dinaro giordano"
+    },
+    "formatTitle": "Formato valuta",
+    "formatSymbolName": "Simbolo",
+    "formatCodeName": "Codice",
+    "formatSymbolLabel": "Simbolo: {value}",
+    "formatCodeLabel": "Codice: {value}"
   },
   "localization": {
     "languageTitle": "Lingua",

+ 1 - 0
messages/it/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Conto Bancario",
     "orgNumber": "N. Organizzazione",
     "paymentTerms": "Condizioni di Pagamento",
+    "netDays": "{days} giorni",
     "dueDate": "Data di Scadenza",
     "serviceImages": "Immagini del Servizio ({count})",
     "serviceVideos": "Video del Servizio ({count})",

+ 1 - 0
messages/lt/pdf.json

@@ -41,6 +41,7 @@
     "paymentInformation": "Mokėjimo informacija",
     "orgNumberLabel": "Įmonės kodas",
     "paymentTermsLabel": "Mokėjimo sąlygos",
+    "netDays": "{days} d.",
     "dueDateLabel": "Mokėjimo terminas",
     "warrantyTitle": "Garantija",
     "warrantyDuration": "Trukmė",

+ 29 - 2
messages/lt/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Filipinų pesas",
       "IDR": "Indonezijos rupija",
       "MYR": "Malaizijos ringitas",
+      "LKR": "Šri Lankos rupija",
       "AED": "JAE dirhamas",
       "SAR": "Saudo Arabijos rijalas",
-      "RUB": "Rusijos rublis"
-    }
+      "RUB": "Rusijos rublis",
+      "TWD": "Taivano doleris",
+      "VND": "Vietnamo donas",
+      "PKR": "Pakistano rupija",
+      "BDT": "Bangladešo taka",
+      "NPR": "Nepalo rupija",
+      "BGN": "Bulgarijos levas",
+      "UAH": "Ukrainos grivina",
+      "CLP": "Čilės pesas",
+      "COP": "Kolumbijos pesas",
+      "ARS": "Argentinos pesas",
+      "PEN": "Peru solis",
+      "EGP": "Egipto svaras",
+      "NGN": "Nigerijos naira",
+      "KES": "Kenijos šilingas",
+      "MAD": "Maroko dirhamas",
+      "GHS": "Ganos sedis",
+      "QAR": "Kataro rialas",
+      "KWD": "Kuveito dinaras",
+      "BHD": "Bahreino dinaras",
+      "OMR": "Omano rialas",
+      "JOD": "Jordanijos dinaras"
+    },
+    "formatTitle": "Valiutos formatas",
+    "formatSymbolName": "Simbolis",
+    "formatCodeName": "Kodas",
+    "formatSymbolLabel": "Simbolis: {value}",
+    "formatCodeLabel": "Kodas: {value}"
   },
   "localization": {
     "languageTitle": "Kalba",

+ 1 - 0
messages/lt/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Banko sąskaita",
     "orgNumber": "Įmonės kodas",
     "paymentTerms": "Mokėjimo sąlygos",
+    "netDays": "{days} d.",
     "dueDate": "Mokėjimo terminas",
     "serviceImages": "Paslaugos vaizdai ({count})",
     "serviceVideos": "Paslaugos vaizdo įrašai ({count})",

+ 1 - 0
messages/nb/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Betalingsinformasjon",
     "orgNumberLabel": "Org.nummer",
     "paymentTermsLabel": "Betalingsbetingelser",
+    "netDays": "{days} Dager",
     "dueDateLabel": "Forfallsdato",
     "warrantyTitle": "Garanti",
     "warrantyDuration": "Varighet",

+ 29 - 2
messages/nb/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Filippinske pesos",
       "IDR": "Indonesiske rupiah",
       "MYR": "Malaysiske ringgit",
+      "LKR": "Srilankiske rupier",
       "AED": "Emiratiske dirham",
       "SAR": "Saudiarabiske riyal",
-      "RUB": "Russisk rubel"
-    }
+      "RUB": "Russisk rubel",
+      "TWD": "Taiwanske dollar",
+      "VND": "Vietnamesisk dong",
+      "PKR": "Pakistanske rupier",
+      "BDT": "Bangladeshisk taka",
+      "NPR": "Nepalesiske rupier",
+      "BGN": "Bulgarsk lev",
+      "UAH": "Ukrainsk hryvnia",
+      "CLP": "Chilenske pesos",
+      "COP": "Colombianske pesos",
+      "ARS": "Argentinske pesos",
+      "PEN": "Peruansk sol",
+      "EGP": "Egyptisk pund",
+      "NGN": "Nigerianske naira",
+      "KES": "Kenyansk shilling",
+      "MAD": "Marokkansk dirham",
+      "GHS": "Ghanesisk cedi",
+      "QAR": "Qatarske riyal",
+      "KWD": "Kuwaitiske dinar",
+      "BHD": "Bahrainske dinar",
+      "OMR": "Omanske rial",
+      "JOD": "Jordanske dinar"
+    },
+    "formatTitle": "Valutaformat",
+    "formatSymbolName": "Symbol",
+    "formatCodeName": "Kode",
+    "formatSymbolLabel": "Symbol: {value}",
+    "formatCodeLabel": "Kode: {value}"
   },
   "localization": {
     "languageTitle": "Språk",

+ 1 - 0
messages/nb/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Bankkontonummer",
     "orgNumber": "Org.nummer",
     "paymentTerms": "Betalingsbetingelser",
+    "netDays": "{days} Dager",
     "dueDate": "Forfallsdato",
     "serviceImages": "Servicebilder ({count})",
     "serviceVideos": "Servicevideoer ({count})",

+ 1 - 0
messages/nl/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Betalingsinformatie",
     "orgNumberLabel": "Org.nr.",
     "paymentTermsLabel": "Betalingsvoorwaarden",
+    "netDays": "{days} dagen",
     "dueDateLabel": "Vervaldatum",
     "warrantyTitle": "Garantie",
     "warrantyDuration": "Duur",

+ 29 - 2
messages/nl/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Filipijnse peso",
       "IDR": "Indonesische roepia",
       "MYR": "Maleisische ringgit",
+      "LKR": "Srilankaanse roepie",
       "AED": "VAE-dirham",
       "SAR": "Saoedische riyal",
-      "RUB": "Russische roebel"
-    }
+      "RUB": "Russische roebel",
+      "TWD": "Taiwanese dollar",
+      "VND": "Vietnamese dong",
+      "PKR": "Pakistaanse roepie",
+      "BDT": "Bengaalse taka",
+      "NPR": "Nepalese roepie",
+      "BGN": "Bulgaarse lev",
+      "UAH": "Oekraïense hryvnia",
+      "CLP": "Chileense peso",
+      "COP": "Colombiaanse peso",
+      "ARS": "Argentijnse peso",
+      "PEN": "Peruaanse sol",
+      "EGP": "Egyptisch pond",
+      "NGN": "Nigeriaanse naira",
+      "KES": "Keniaanse shilling",
+      "MAD": "Marokkaanse dirham",
+      "GHS": "Ghanese cedi",
+      "QAR": "Qatarese rial",
+      "KWD": "Koeweitse dinar",
+      "BHD": "Bahreinse dinar",
+      "OMR": "Omaanse rial",
+      "JOD": "Jordaanse dinar"
+    },
+    "formatTitle": "Valutaformaat",
+    "formatSymbolName": "Symbool",
+    "formatCodeName": "Code",
+    "formatSymbolLabel": "Symbool: {value}",
+    "formatCodeLabel": "Code: {value}"
   },
   "localization": {
     "languageTitle": "Taal",

+ 1 - 0
messages/nl/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Bankrekening",
     "orgNumber": "KvK-nummer",
     "paymentTerms": "Betalingsvoorwaarden",
+    "netDays": "{days} dagen",
     "dueDate": "Vervaldatum",
     "serviceImages": "Serviceafbeeldingen ({count})",
     "serviceVideos": "Servicevideo's ({count})",

+ 1 - 0
messages/pl/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Informacje o płatności",
     "orgNumberLabel": "Nr org.",
     "paymentTermsLabel": "Warunki płatności",
+    "netDays": "{days} dni",
     "dueDateLabel": "Termin płatności",
     "warrantyTitle": "Gwarancja",
     "warrantyDuration": "Czas trwania",

+ 29 - 2
messages/pl/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Peso filipinskie",
       "IDR": "Rupia indonezyjska",
       "MYR": "Ringgit malezyjski",
+      "LKR": "Rupia lankijska",
       "AED": "Dirham ZEA",
       "SAR": "Rial saudyjski",
-      "RUB": "Rubel rosyjski"
-    }
+      "RUB": "Rubel rosyjski",
+      "TWD": "Dolar tajwanski",
+      "VND": "Dong wietnamski",
+      "PKR": "Rupia pakistanska",
+      "BDT": "Taka bangladeska",
+      "NPR": "Rupia nepalska",
+      "BGN": "Lew bulgarski",
+      "UAH": "Hrywna ukrainska",
+      "CLP": "Peso chilijskie",
+      "COP": "Peso kolumbijskie",
+      "ARS": "Peso argentynskie",
+      "PEN": "Sol peruwianski",
+      "EGP": "Funt egipski",
+      "NGN": "Naira nigeryjska",
+      "KES": "Szyling kenijski",
+      "MAD": "Dirham marokanski",
+      "GHS": "Cedi ghanskie",
+      "QAR": "Rial katarski",
+      "KWD": "Dinar kuwejcki",
+      "BHD": "Dinar bahrajnski",
+      "OMR": "Rial omanski",
+      "JOD": "Dinar jordanski"
+    },
+    "formatTitle": "Format waluty",
+    "formatSymbolName": "Symbol",
+    "formatCodeName": "Kod",
+    "formatSymbolLabel": "Symbol: {value}",
+    "formatCodeLabel": "Kod: {value}"
   },
   "localization": {
     "languageTitle": "Język",

+ 1 - 0
messages/pl/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Konto bankowe",
     "orgNumber": "Nr org.",
     "paymentTerms": "Warunki płatności",
+    "netDays": "{days} dni",
     "dueDate": "Termin płatności",
     "serviceImages": "Zdjęcia serwisowe ({count})",
     "serviceVideos": "Filmy serwisowe ({count})",

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

@@ -40,6 +40,7 @@
     "paymentInformation": "Informações de pagamento",
     "orgNumberLabel": "Nº da org.",
     "paymentTermsLabel": "Condições de pagamento",
+    "netDays": "{days} dias",
     "dueDateLabel": "Data de vencimento",
     "warrantyTitle": "Garantia",
     "warrantyDuration": "Duração",

+ 29 - 2
messages/pt-BR/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Peso filipino",
       "IDR": "Rupia indonesia",
       "MYR": "Ringgit malaio",
+      "LKR": "Rupia do Sri Lanka",
       "AED": "Dirham dos EAU",
       "SAR": "Rial saudita",
-      "RUB": "Rublo russo"
-    }
+      "RUB": "Rublo russo",
+      "TWD": "Dolar taiwanes",
+      "VND": "Dong vietnamita",
+      "PKR": "Rupia paquistanesa",
+      "BDT": "Taka bengalesa",
+      "NPR": "Rupia nepalesa",
+      "BGN": "Lev bulgaro",
+      "UAH": "Hryvnia ucraniana",
+      "CLP": "Peso chileno",
+      "COP": "Peso colombiano",
+      "ARS": "Peso argentino",
+      "PEN": "Sol peruano",
+      "EGP": "Libra egipcia",
+      "NGN": "Naira nigeriana",
+      "KES": "Xelim queniano",
+      "MAD": "Dirham marroquino",
+      "GHS": "Cedi ganes",
+      "QAR": "Rial catariano",
+      "KWD": "Dinar kuwaitiano",
+      "BHD": "Dinar do Bahrein",
+      "OMR": "Rial omanense",
+      "JOD": "Dinar jordaniano"
+    },
+    "formatTitle": "Formato da moeda",
+    "formatSymbolName": "Simbolo",
+    "formatCodeName": "Codigo",
+    "formatSymbolLabel": "Simbolo: {value}",
+    "formatCodeLabel": "Codigo: {value}"
   },
   "localization": {
     "languageTitle": "Idioma",

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

@@ -52,6 +52,7 @@
     "bankAccount": "Conta Bancária",
     "orgNumber": "CNPJ",
     "paymentTerms": "Condições de Pagamento",
+    "netDays": "{days} dias",
     "dueDate": "Data de Vencimento",
     "serviceImages": "Imagens do Serviço ({count})",
     "serviceVideos": "Vídeos do Serviço ({count})",

+ 1 - 0
messages/ru/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Платёжная информация",
     "orgNumberLabel": "Номер организации",
     "paymentTermsLabel": "Условия оплаты",
+    "netDays": "{days} дней",
     "dueDateLabel": "Срок оплаты",
     "warrantyTitle": "Гарантия",
     "warrantyDuration": "Срок",

+ 29 - 2
messages/ru/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Филиппинское песо",
       "IDR": "Индонезийская рупия",
       "MYR": "Малайзийский ринггит",
+      "LKR": "Шри-ланкийская рупия",
       "AED": "Дирхам ОАЭ",
       "SAR": "Саудовский риял",
-      "RUB": "Российский рубль"
-    }
+      "RUB": "Российский рубль",
+      "TWD": "Тайваньский доллар",
+      "VND": "Вьетнамский донг",
+      "PKR": "Пакистанская рупия",
+      "BDT": "Бангладешская така",
+      "NPR": "Непальская рупия",
+      "BGN": "Болгарский лев",
+      "UAH": "Украинская гривна",
+      "CLP": "Чилийское песо",
+      "COP": "Колумбийское песо",
+      "ARS": "Аргентинское песо",
+      "PEN": "Перуанский соль",
+      "EGP": "Египетский фунт",
+      "NGN": "Нигерийская найра",
+      "KES": "Кенийский шиллинг",
+      "MAD": "Марокканский дирхам",
+      "GHS": "Ганский седи",
+      "QAR": "Катарский риал",
+      "KWD": "Кувейтский динар",
+      "BHD": "Бахрейнский динар",
+      "OMR": "Оманский риал",
+      "JOD": "Иорданский динар"
+    },
+    "formatTitle": "Формат валюты",
+    "formatSymbolName": "Символ",
+    "formatCodeName": "Код",
+    "formatSymbolLabel": "Символ: {value}",
+    "formatCodeLabel": "Код: {value}"
   },
   "localization": {
     "languageTitle": "Язык",

+ 1 - 0
messages/ru/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Банковский счёт",
     "orgNumber": "Номер организации",
     "paymentTerms": "Условия оплаты",
+    "netDays": "{days} дней",
     "dueDate": "Срок оплаты",
     "serviceImages": "Фотографии обслуживания ({count})",
     "serviceVideos": "Видео обслуживания ({count})",

+ 1 - 0
messages/tr/pdf.json

@@ -40,6 +40,7 @@
     "paymentInformation": "Ödeme bilgileri",
     "orgNumberLabel": "Org. No.",
     "paymentTermsLabel": "Ödeme koşulları",
+    "netDays": "{days} Gün",
     "dueDateLabel": "Vade tarihi",
     "warrantyTitle": "Garanti",
     "warrantyDuration": "Süre",

+ 29 - 2
messages/tr/settings.json

@@ -670,10 +670,37 @@
       "PHP": "Filipin Pesosu",
       "IDR": "Endonezya Rupisi",
       "MYR": "Malezya Ringgiti",
+      "LKR": "Sri Lanka Rupisi",
       "AED": "BAE Dirhemi",
       "SAR": "Suudi Riyali",
-      "RUB": "Rus Rublesi"
-    }
+      "RUB": "Rus Rublesi",
+      "TWD": "Tayvan Doları",
+      "VND": "Vietnam Dongu",
+      "PKR": "Pakistan Rupisi",
+      "BDT": "Bangladeş Takası",
+      "NPR": "Nepal Rupisi",
+      "BGN": "Bulgar Levası",
+      "UAH": "Ukrayna Grivnası",
+      "CLP": "Şili Pesosu",
+      "COP": "Kolombiya Pesosu",
+      "ARS": "Arjantin Pesosu",
+      "PEN": "Peru Solu",
+      "EGP": "Mısır Lirası",
+      "NGN": "Nijerya Nairası",
+      "KES": "Kenya Şilini",
+      "MAD": "Fas Dirhemi",
+      "GHS": "Gana Sedisi",
+      "QAR": "Katar Riyali",
+      "KWD": "Kuveyt Dinarı",
+      "BHD": "Bahreyn Dinarı",
+      "OMR": "Umman Riyali",
+      "JOD": "Ürdün Dinarı"
+    },
+    "formatTitle": "Para Birimi Formatı",
+    "formatSymbolName": "Sembol",
+    "formatCodeName": "Kod",
+    "formatSymbolLabel": "Sembol: {value}",
+    "formatCodeLabel": "Kod: {value}"
   },
   "localization": {
     "languageTitle": "Dil",

+ 1 - 0
messages/tr/share.json

@@ -52,6 +52,7 @@
     "bankAccount": "Banka Hesabı",
     "orgNumber": "Org. Numarası",
     "paymentTerms": "Ödeme Koşulları",
+    "netDays": "{days} Gün",
     "dueDate": "Son Ödeme Tarihi",
     "serviceImages": "Servis Görselleri ({count})",
     "serviceVideos": "Servis Videoları ({count})",

+ 6 - 0
src/app/(authenticated)/layout.tsx

@@ -8,6 +8,7 @@ import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { WhiteLabelCtaProvider } from "@/components/white-label-cta-context";
 import { DateSettingsProvider } from "@/components/date-settings-context";
+import { CurrencySettingsProvider } from "@/components/currency-settings-context";
 import { getCachedMembership } from "@/lib/cached-session";
 import { hasPermission, PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { OnlineTracker } from "@/components/online-tracker";
@@ -115,6 +116,10 @@ export default async function DashboardLayout({
         timeFormat={data.timeFormat}
         timezone={data.timezone}
       >
+      <CurrencySettingsProvider
+        currencyCode={data.currencyCode}
+        currencyFormat={data.currencyFormat}
+      >
       <ConfirmProvider>
         <AppSidebar
           companyLogo={data.companyLogo}
@@ -135,6 +140,7 @@ export default async function DashboardLayout({
         <OnlineTracker />
         <InstallBanner />
       </ConfirmProvider>
+      </CurrencySettingsProvider>
       </DateSettingsProvider>
     </SidebarProvider>
     <MobileBottomNav isSuperAdmin={data.isSuperAdmin} />

+ 60 - 2
src/app/(authenticated)/settings/localization/localization-settings.tsx

@@ -80,8 +80,30 @@ const CURRENCIES = [
   { code: 'PHP', key: 'PHP' },
   { code: 'IDR', key: 'IDR' },
   { code: 'MYR', key: 'MYR' },
+  { code: 'LKR', key: 'LKR' },
+  { code: 'TWD', key: 'TWD' },
+  { code: 'VND', key: 'VND' },
+  { code: 'PKR', key: 'PKR' },
+  { code: 'BDT', key: 'BDT' },
+  { code: 'NPR', key: 'NPR' },
+  { code: 'BGN', key: 'BGN' },
+  { code: 'UAH', key: 'UAH' },
+  { code: 'CLP', key: 'CLP' },
+  { code: 'COP', key: 'COP' },
+  { code: 'ARS', key: 'ARS' },
+  { code: 'PEN', key: 'PEN' },
+  { code: 'EGP', key: 'EGP' },
+  { code: 'NGN', key: 'NGN' },
+  { code: 'KES', key: 'KES' },
+  { code: 'MAD', key: 'MAD' },
+  { code: 'GHS', key: 'GHS' },
   { code: 'AED', key: 'AED' },
   { code: 'SAR', key: 'SAR' },
+  { code: 'QAR', key: 'QAR' },
+  { code: 'KWD', key: 'KWD' },
+  { code: 'BHD', key: 'BHD' },
+  { code: 'OMR', key: 'OMR' },
+  { code: 'JOD', key: 'JOD' },
   { code: 'RUB', key: 'RUB' },
 ]
 
@@ -155,6 +177,9 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
 
   // Currency
   const [currencyCode, setCurrencyCode] = useState(settings[SETTING_KEYS.CURRENCY_CODE] || 'USD')
+  const [currencyFormat, setCurrencyFormat] = useState<'symbol' | 'code'>(
+    settings[SETTING_KEYS.CURRENCY_FORMAT] === 'code' ? 'code' : 'symbol'
+  )
 
   // Date & Time
   const [dateFormat, setDateFormat] = useState(
@@ -206,6 +231,7 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
     setSaving(true)
     await setSettings({
       [SETTING_KEYS.CURRENCY_CODE]: currencyCode,
+      [SETTING_KEYS.CURRENCY_FORMAT]: currencyFormat,
       [SETTING_KEYS.DATE_FORMAT]: dateFormat,
       [SETTING_KEYS.TIME_FORMAT]: timeFormat,
       [SETTING_KEYS.TIMEZONE]: timezone,
@@ -363,8 +389,40 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
                     </Command>
                   </PopoverContent>
                 </Popover>
-                <p className="text-xs text-muted-foreground">
-                  {t('currency.previewLabel', { value: formatCurrency(1234.56, currencyCode) })}
+              </div>
+
+              <div className="space-y-2">
+                <Label>{t('currency.formatTitle')}</Label>
+                <Select
+                  value={currencyFormat}
+                  onValueChange={(v) => setCurrencyFormat(v === 'code' ? 'code' : 'symbol')}
+                >
+                  <SelectTrigger className="w-full">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="symbol" suppressHydrationWarning>
+                      {mounted
+                        ? t('currency.formatSymbolLabel', {
+                            value: formatCurrency(1000, currencyCode, 'symbol'),
+                          })
+                        : t('currency.formatSymbolName')}
+                    </SelectItem>
+                    <SelectItem value="code" suppressHydrationWarning>
+                      {mounted
+                        ? t('currency.formatCodeLabel', {
+                            value: formatCurrency(1000, currencyCode, 'code'),
+                          })
+                        : t('currency.formatCodeName')}
+                    </SelectItem>
+                  </SelectContent>
+                </Select>
+                <p className="text-xs text-muted-foreground" suppressHydrationWarning>
+                  {mounted
+                    ? t('currency.previewLabel', {
+                        value: formatCurrency(1234.56, currencyCode, currencyFormat),
+                      })
+                    : ' '}
                 </p>
               </div>
             </div>

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

@@ -185,6 +185,7 @@ export function InvoiceView({
   record,
   workshop,
   currencyCode,
+  currencyFormat = 'symbol',
   orgId,
   token,
   enabledProviders = [],
@@ -210,6 +211,7 @@ export function InvoiceView({
   record: InvoiceRecord
   workshop: { name: string; address: string; phone: string; email: string }
   currencyCode: string
+  currencyFormat?: 'symbol' | 'code'
   orgId: string
   token: string
   enabledProviders?: string[]
@@ -426,7 +428,7 @@ export function InvoiceView({
       return
     }
     if (amount > balanceDue + 0.01) {
-      setPaymentError(t('errorExceedsBalance', { amount: formatCurrency(balanceDue, currencyCode) }))
+      setPaymentError(t('errorExceedsBalance', { amount: formatCurrency(balanceDue, currencyCode, currencyFormat) }))
       return
     }
 
@@ -483,7 +485,7 @@ export function InvoiceView({
             {t('paymentReceived')}
           </p>
           <p className="text-sm text-emerald-600 dark:text-emerald-500">
-            {t('paymentApplied', { amount: formatCurrency(paymentSuccess.amount, currencyCode) })}
+            {t('paymentApplied', { amount: formatCurrency(paymentSuccess.amount, currencyCode, currencyFormat) })}
           </p>
         </div>
       )}
@@ -506,7 +508,7 @@ export function InvoiceView({
                 <span className="text-sm font-semibold">{t('balanceDue')}</span>
               </div>
               <span className="text-lg font-bold text-white">
-                {formatCurrency(balanceDue, currencyCode)}
+                {formatCurrency(balanceDue, currencyCode, currencyFormat)}
               </span>
             </div>
           </div>
@@ -846,10 +848,10 @@ export function InvoiceView({
                               <td className="p-2">{p.name}</td>
                               <td className="p-2 text-right">{p.quantity}</td>
                               <td className="p-2 text-right">
-                                {formatCurrency(netUnitPrice, currencyCode)}
+                                {formatCurrency(netUnitPrice, currencyCode, currencyFormat)}
                               </td>
                               <td className="p-2 text-right font-medium">
-                                {formatCurrency(netLineValue, currencyCode)}
+                                {formatCurrency(netLineValue, currencyCode, currencyFormat)}
                               </td>
                             </tr>
                           )
@@ -883,9 +885,9 @@ export function InvoiceView({
                             <tr key={i}>
                               <td className="p-2">{l.description}</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(netRate, currencyCode) : t('ratePerHour', { rate: formatCurrency(netRate, currencyCode) })}</td>
+                              <td className="p-2 text-right">{l.pricingType === 'service' ? formatCurrency(netRate, currencyCode, currencyFormat) : t('ratePerHour', { rate: formatCurrency(netRate, currencyCode, currencyFormat) })}</td>
                               <td className="p-2 text-right font-medium">
-                                {formatCurrency(netLineValue, currencyCode)}
+                                {formatCurrency(netLineValue, currencyCode, currencyFormat)}
                               </td>
                             </tr>
                           )
@@ -904,15 +906,15 @@ export function InvoiceView({
                       <>
                         <div className="flex justify-between text-sm">
                           <span className="text-gray-500">{t('parts')}</span>
-                          <span>{formatCurrency(partsSubtotal, currencyCode)}</span>
+                          <span>{formatCurrency(partsSubtotal, currencyCode, currencyFormat)}</span>
                         </div>
                         <div className="flex justify-between text-sm">
                           <span className="text-gray-500">{t('labor')}</span>
-                          <span>{formatCurrency(laborSubtotal, currencyCode)}</span>
+                          <span>{formatCurrency(laborSubtotal, currencyCode, currencyFormat)}</span>
                         </div>
                         <div className="flex justify-between text-sm">
                           <span className="text-gray-500">{t('subtotal')}</span>
-                          <span>{formatCurrency(computedSubtotal, currencyCode)}</span>
+                          <span>{formatCurrency(computedSubtotal, currencyCode, currencyFormat)}</span>
                         </div>
                       </>
                     )}
@@ -922,7 +924,7 @@ export function InvoiceView({
                           {record.discountType === 'percentage' ? t('discountPercent', { percent: record.discountValue }) : t('discount')}
                         </span>
                         <span className="text-red-500">
-                          {formatCurrency(-displayDiscountAmount, currencyCode)}
+                          {formatCurrency(-displayDiscountAmount, currencyCode, currencyFormat)}
                         </span>
                       </div>
                     )}
@@ -933,7 +935,7 @@ export function InvoiceView({
                             ? `${taxLabel} (${record.taxRate}%)`
                             : t('tax', { rate: record.taxRate })}
                         </span>
-                        <span>{formatCurrency(computedTax, currencyCode)}</span>
+                        <span>{formatCurrency(computedTax, currencyCode, currencyFormat)}</span>
                       </div>
                     )}
                     <div
@@ -945,7 +947,7 @@ export function InvoiceView({
                       >
                         <span>{t('total')}</span>
                         <span style={totalPaid > 0 ? undefined : { color: primaryColor }}>
-                          {formatCurrency(displayTotal, currencyCode)}
+                          {formatCurrency(displayTotal, currencyCode, currencyFormat)}
                         </span>
                       </div>
                     </div>
@@ -953,7 +955,7 @@ export function InvoiceView({
                       <>
                         <div className="flex justify-between text-sm text-emerald-600">
                           <span>{t('paid')}</span>
-                          <span>{formatCurrency(-totalPaid, currencyCode)}</span>
+                          <span>{formatCurrency(-totalPaid, currencyCode, currencyFormat)}</span>
                         </div>
                         {balanceDue <= 0 ? (
                           <div className="rounded-lg bg-emerald-50 px-4 py-3 dark:bg-emerald-900/20">
@@ -967,7 +969,7 @@ export function InvoiceView({
                             <div className="flex items-center justify-between gap-2 text-lg font-bold">
                               <span className="whitespace-nowrap">{t('amountDue')}</span>
                               <span className="text-amber-700 dark:text-amber-400">
-                                {formatCurrency(balanceDue, currencyCode)}
+                                {formatCurrency(balanceDue, currencyCode, currencyFormat)}
                               </span>
                             </div>
                           </div>
@@ -1124,7 +1126,7 @@ export function InvoiceView({
                       return netDays !== null && netDays > 0 ? (
                         <div>
                           <p className="text-xs text-gray-500 dark:text-gray-400">{t('paymentTerms')}</p>
-                          <p className="font-medium">Net {netDays} Days</p>
+                          <p className="font-medium">{t('netDays', { days: netDays })}</p>
                         </div>
                       ) : invoiceSettings.paymentTerms ? (
                         <div>

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

@@ -79,6 +79,7 @@ export default async function PublicInvoicePage({
             "workshop.email",
             "workshop.logo",
             "workshop.currencyCode",
+            "workshop.currencyFormat",
             "invoice.bankAccount",
             "invoice.orgNumber",
             "invoice.paymentTerms",
@@ -143,6 +144,7 @@ export default async function PublicInvoicePage({
   };
 
   const currencyCode = settingsMap["workshop.currencyCode"] || "USD";
+  const currencyFormat: "symbol" | "code" = settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol";
 
   const invoiceSettings = {
     bankAccount: settingsMap["invoice.bankAccount"] || "",
@@ -202,6 +204,7 @@ export default async function PublicInvoicePage({
       record={publicRecord}
       workshop={workshop}
       currencyCode={currencyCode}
+      currencyFormat={currencyFormat}
       orgId={orgId}
       token={token}
       enabledProviders={enabledProviders}

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

@@ -66,6 +66,7 @@ export default async function PublicQuotePage({
             "workshop.email",
             "workshop.logo",
             "workshop.currencyCode",
+            "workshop.currencyFormat",
             "workshop.dateFormat",
             "workshop.timezone",
             "quote.primaryColor",
@@ -110,6 +111,7 @@ export default async function PublicQuotePage({
   };
 
   const currencyCode = settingsMap["workshop.currencyCode"] || "USD";
+  const currencyFormat: "symbol" | "code" = settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol";
 
   // Rewrite logo URL for public access
   const rawLogoUrl = settingsMap["workshop.logo"] || "";
@@ -161,6 +163,7 @@ export default async function PublicQuotePage({
       quote={quote}
       workshop={workshop}
       currencyCode={currencyCode}
+      currencyFormat={currencyFormat}
       orgId={orgId}
       token={token}
       logoUrl={logoUrl}

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

@@ -134,6 +134,7 @@ export function QuoteView({
   quote,
   workshop,
   currencyCode,
+  currencyFormat = 'symbol',
   orgId,
   token,
   logoUrl,
@@ -153,7 +154,8 @@ export function QuoteView({
 }: {
   quote: QuoteRecord;
   workshop: { name: string; address: string; phone: string; email: string };
-  currencyCode: string;
+  currencyCode: string
+  currencyFormat?: 'symbol' | 'code';
   orgId: string;
   token: string;
   logoUrl?: string;
@@ -570,8 +572,8 @@ export function QuoteView({
                               <td className="p-2 font-mono text-xs">{p.partNumber || "-"}</td>
                               <td className="p-2">{p.name}</td>
                               <td className="p-2 text-right">{p.quantity}</td>
-                              <td className="p-2 text-right">{formatCurrency(netUnitPrice, currencyCode)}</td>
-                              <td className="p-2 text-right font-medium">{formatCurrency(netLineValue, currencyCode)}</td>
+                              <td className="p-2 text-right">{formatCurrency(netUnitPrice, currencyCode, currencyFormat)}</td>
+                              <td className="p-2 text-right font-medium">{formatCurrency(netLineValue, currencyCode, currencyFormat)}</td>
                             </tr>
                           )
                         })}
@@ -604,8 +606,8 @@ export function QuoteView({
                             <tr key={i} className={l.excluded ? "line-through text-gray-400" : ""}>
                               <td className="p-2">{l.description}</td>
                               <td className="p-2 text-right">{l.pricingType === 'service' ? `${l.hours} ${t('unit')}` : `${l.hours} ${t('hrs')}`}</td>
-                              <td className="p-2 text-right">{l.pricingType === 'service' ? formatCurrency(netRate, currencyCode) : t('ratePerHour', { rate: formatCurrency(netRate, currencyCode) })}</td>
-                              <td className="p-2 text-right font-medium">{formatCurrency(netLineValue, currencyCode)}</td>
+                              <td className="p-2 text-right">{l.pricingType === 'service' ? formatCurrency(netRate, currencyCode, currencyFormat) : t('ratePerHour', { rate: formatCurrency(netRate, currencyCode, currencyFormat) })}</td>
+                              <td className="p-2 text-right font-medium">{formatCurrency(netLineValue, currencyCode, currencyFormat)}</td>
                             </tr>
                           )
                         })}
@@ -645,19 +647,19 @@ export function QuoteView({
                         {quote.laborItems.length > 0 && (
                           <div className="flex justify-between text-sm">
                             <span className="text-gray-500">{t('labor')}</span>
-                            <span>{formatCurrency(laborTotal, currencyCode)}</span>
+                            <span>{formatCurrency(laborTotal, currencyCode, currencyFormat)}</span>
                           </div>
                         )}
                         {quote.partItems.length > 0 && (
                           <div className="flex justify-between text-sm">
                             <span className="text-gray-500">{t('parts')}</span>
-                            <span>{formatCurrency(partsTotal, currencyCode)}</span>
+                            <span>{formatCurrency(partsTotal, currencyCode, currencyFormat)}</span>
                           </div>
                         )}
                         {sub > 0 && (
                           <div className="flex justify-between text-sm">
                             <span className="text-gray-500">{t('subtotal')}</span>
-                            <span>{formatCurrency(sub, currencyCode)}</span>
+                            <span>{formatCurrency(sub, currencyCode, currencyFormat)}</span>
                           </div>
                         )}
                         {disc > 0 && (
@@ -665,7 +667,7 @@ export function QuoteView({
                             <span className="text-gray-500">
                               {quote.discountType === "percentage" ? t('discountPercent', { percent: quote.discountValue }) : t('discount')}
                             </span>
-                            <span className="text-red-500">{formatCurrency(-disc, currencyCode)}</span>
+                            <span className="text-red-500">{formatCurrency(-disc, currencyCode, currencyFormat)}</span>
                           </div>
                         )}
                         {quote.taxRate > 0 && (
@@ -675,13 +677,13 @@ export function QuoteView({
                                 ? `${taxLabel} (${quote.taxRate}%)`
                                 : t('tax', { rate: quote.taxRate })}
                             </span>
-                            <span>{formatCurrency(tax, currencyCode)}</span>
+                            <span>{formatCurrency(tax, currencyCode, currencyFormat)}</span>
                           </div>
                         )}
                         <div className="border-t pt-2" style={{ borderColor: primaryColor }}>
                           <div className="flex justify-between text-lg font-bold">
                             <span>{t('total')}</span>
-                            <span style={{ color: primaryColor }}>{formatCurrency(total, currencyCode)}</span>
+                            <span style={{ color: primaryColor }}>{formatCurrency(total, currencyCode, currencyFormat)}</span>
                           </div>
                         </div>
                       </>

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

@@ -178,6 +178,7 @@ export async function GET(
         email: settingsMap["workshop.email"] || "",
       },
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
+      currencyFormat: (settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol") as "symbol" | "code",
       logoDataUri,
       torqvoiceLogoDataUri,
       dateFormat: settingsMap["workshop.dateFormat"] || undefined,

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

@@ -197,6 +197,7 @@ export async function GET(
       showOrgNumber: settingsMap["invoice.showOrgNumber"] === "true",
       dueDays: Number(settingsMap["invoice.dueDays"]) || 0,
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
+      currencyFormat: (settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol") as "symbol" | "code",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
       dateFormat: settingsMap["workshop.dateFormat"] || undefined,
       timezone: settingsMap["workshop.timezone"] || undefined,

+ 1 - 0
src/app/api/protected/vehicles/[id]/service-history-pdf/route.ts

@@ -131,6 +131,7 @@ export async function GET(
 
     const invoiceSettings = {
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
+      currencyFormat: (settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol") as "symbol" | "code",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
       dateFormat: settingsMap["workshop.dateFormat"] || undefined,
       timezone: settingsMap["workshop.timezone"] || undefined,

+ 25 - 2
src/app/api/public/share/invoice/[orgId]/[token]/checkout/route.ts

@@ -4,6 +4,7 @@ import { db } from "@/lib/db";
 import { getPaymentProvider, getEnabledProviders } from "@/lib/payment-providers";
 import { rateLimit } from "@/lib/rate-limit";
 import { resolvePortalOrg } from "@/lib/portal-slug";
+import { calculateTotals } from "@/lib/tax";
 
 const checkoutSchema = z.object({
   provider: z.enum(["stripe", "vipps", "paypal"]),
@@ -28,6 +29,8 @@ export async function POST(
       where: { publicToken: token },
       include: {
         payments: true,
+        partItems: true,
+        laborItems: true,
         vehicle: {
           select: { organizationId: true },
         },
@@ -49,9 +52,29 @@ export async function POST(
 
     const { provider, amount } = parsed.data;
 
-    // Calculate balance due
+    // Calculate balance due — mirrors invoice-view.tsx so the server's
+    // notion of total matches what the customer sees on the share page.
+    const partsSubtotal = record.partItems.reduce((sum, p) => sum + p.total, 0);
+    const laborSubtotal = record.laborItems.reduce((sum, l) => sum + l.total, 0);
+    const computedSubtotal = partsSubtotal + laborSubtotal;
+    const computedDiscount =
+      record.discountType === "percentage"
+        ? computedSubtotal * (record.discountValue / 100)
+        : record.discountType === "fixed"
+          ? Math.min(record.discountValue, computedSubtotal)
+          : 0;
+    const { totalAmount: computedTotal } = calculateTotals({
+      subtotal: computedSubtotal,
+      discountAmount: computedDiscount,
+      taxRate: record.taxRate,
+      taxInclusive: record.taxInclusive ?? false,
+    });
     const displayTotal =
-      record.totalAmount > 0 ? record.totalAmount : record.cost;
+      record.totalAmount > 0
+        ? record.totalAmount
+        : computedTotal > 0
+          ? computedTotal
+          : record.cost;
     const paidFromPayments = record.payments.reduce((sum, p) => sum + p.amount, 0);
     const totalPaid = record.manuallyPaid ? displayTotal : paidFromPayments;
     const balanceDue = displayTotal - totalPaid;

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

@@ -196,6 +196,7 @@ export async function GET(
         email: settingsMap["workshop.email"] || "",
       },
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
+      currencyFormat: (settingsMap["workshop.currencyFormat"] === "code" ? "code" : "symbol") as "symbol" | "code",
       logoDataUri,
       torqvoiceLogoDataUri,
       dateFormat: settingsMap["workshop.dateFormat"] || undefined,

+ 63 - 0
src/components/currency-settings-context.tsx

@@ -0,0 +1,63 @@
+"use client";
+
+import { createContext, useCallback, useContext } from "react";
+import {
+  formatCurrency as formatCurrencyRaw,
+  getCurrencySymbol as getCurrencySymbolRaw,
+  DEFAULT_CURRENCY_FORMAT,
+  type CurrencyFormat,
+} from "@/lib/format";
+
+interface CurrencySettings {
+  currencyCode: string;
+  currencyFormat: CurrencyFormat;
+}
+
+const defaultSettings: CurrencySettings = {
+  currencyCode: "USD",
+  currencyFormat: DEFAULT_CURRENCY_FORMAT,
+};
+
+const CurrencySettingsContext = createContext<CurrencySettings>(defaultSettings);
+
+export function CurrencySettingsProvider({
+  currencyCode,
+  currencyFormat,
+  children,
+}: {
+  currencyCode?: string;
+  currencyFormat?: string;
+  children: React.ReactNode;
+}) {
+  const value: CurrencySettings = {
+    currencyCode: currencyCode || "USD",
+    currencyFormat: currencyFormat === "code" ? "code" : "symbol",
+  };
+  return (
+    <CurrencySettingsContext.Provider value={value}>
+      {children}
+    </CurrencySettingsContext.Provider>
+  );
+}
+
+export function useCurrencySettings() {
+  return useContext(CurrencySettingsContext);
+}
+
+/**
+ * Returns a formatter bound to the org's currency code and format preference.
+ * Use this in client components instead of importing formatCurrency directly.
+ */
+export function useFormatCurrency() {
+  const { currencyCode, currencyFormat } = useCurrencySettings();
+  return useCallback(
+    (amount: number, codeOverride?: string) =>
+      formatCurrencyRaw(amount, codeOverride || currencyCode, currencyFormat),
+    [currencyCode, currencyFormat]
+  );
+}
+
+export function useCurrencySymbol() {
+  const { currencyCode, currencyFormat } = useCurrencySettings();
+  return getCurrencySymbolRaw(currencyCode, currencyFormat);
+}

+ 5 - 1
src/features/email/Actions/emailActions.ts

@@ -28,6 +28,7 @@ async function getWorkshopSettings(organizationId: string) {
             "workshop.email",
             "workshop.logo",
             "workshop.currencyCode",
+            "workshop.currencyFormat",
             "workshop.emailFromName",
             "workshop.emailEnabled",
             "invoice.primaryColor",
@@ -98,6 +99,7 @@ export async function sendQuoteEmail(input: {
 
     const logoDataUri = await loadLogoDataUri(settings["workshop.logo"]);
     const currencyCode = settings["workshop.currencyCode"] || "USD";
+    const currencyFormat: "symbol" | "code" = settings["workshop.currencyFormat"] === "code" ? "code" : "symbol";
     const fromName = settings["workshop.emailFromName"] || settings["workshop.name"] || "Workshop";
 
     const template = {
@@ -118,6 +120,7 @@ export async function sendQuoteEmail(input: {
         email: settings["workshop.email"] || "",
       },
       currencyCode,
+      currencyFormat,
       logoDataUri,
       template,
     }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -236,6 +239,7 @@ export async function sendInvoiceEmail(input: {
 
     const logoDataUri = await loadLogoDataUri(settings["workshop.logo"]);
     const currencyCode = settings["workshop.currencyCode"] || "USD";
+    const currencyFormat: "symbol" | "code" = settings["workshop.currencyFormat"] === "code" ? "code" : "symbol";
     const fromName = settings["workshop.emailFromName"] || settings["workshop.name"] || "Workshop";
 
     const invoiceTemplate = {
@@ -255,7 +259,7 @@ export async function sendInvoiceEmail(input: {
         phone: settings["workshop.phone"] || "",
         email: settings["workshop.email"] || "",
       },
-      invoiceSettings: { currencyCode },
+      invoiceSettings: { currencyCode, currencyFormat },
       logoDataUri,
       template: invoiceTemplate,
     }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any

+ 14 - 12
src/features/quotes/Components/QuotePDF.tsx

@@ -81,6 +81,7 @@ export function QuotePDF({
   data,
   workshop,
   currencyCode = 'USD',
+  currencyFormat = 'symbol',
   logoDataUri,
   torqvoiceLogoDataUri,
   dateFormat,
@@ -97,6 +98,7 @@ export function QuotePDF({
   data: QuoteData
   workshop?: WorkshopInfo
   currencyCode?: string
+  currencyFormat?: 'symbol' | 'code'
   logoDataUri?: string
   torqvoiceLogoDataUri?: string
   dateFormat?: string
@@ -463,10 +465,10 @@ export function QuotePDF({
                   {p.quantity}
                 </Text>
                 <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                  {formatCurrency(netUnitPrice, currencyCode)}
+                  {formatCurrency(netUnitPrice, currencyCode, currencyFormat)}
                 </Text>
                 <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(p.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                  {formatCurrency(netLineValue, currencyCode)}
+                  {formatCurrency(netLineValue, currencyCode, currencyFormat)}
                 </Text>
               </View>
             )
@@ -508,13 +510,13 @@ export function QuotePDF({
                 </Text>
                 <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
                   {isService
-                    ? formatCurrency(netRate, currencyCode)
+                    ? formatCurrency(netRate, currencyCode, currencyFormat)
                     : labels.ratePerHour
-                      ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(netRate, currencyCode) })
-                      : `${formatCurrency(netRate, currencyCode)}/hr`}
+                      ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(netRate, currencyCode, currencyFormat) })
+                      : `${formatCurrency(netRate, currencyCode, currencyFormat)}/hr`}
                 </Text>
                 <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right', ...(l.excluded ? { textDecoration: 'line-through' } : {}) }}>
-                  {formatCurrency(netLineValue, currencyCode)}
+                  {formatCurrency(netLineValue, currencyCode, currencyFormat)}
                 </Text>
               </View>
             )
@@ -556,7 +558,7 @@ export function QuotePDF({
           <View style={styles.totalRow}>
             <Text style={styles.totalLabel}>{labels.labor || 'Labor'}</Text>
             <Text style={styles.totalValue}>
-              {formatCurrency(laborTotalDisplay, currencyCode)}
+              {formatCurrency(laborTotalDisplay, currencyCode, currencyFormat)}
             </Text>
           </View>
         )}
@@ -564,14 +566,14 @@ export function QuotePDF({
           <View style={styles.totalRow}>
             <Text style={styles.totalLabel}>{labels.parts || 'Parts'}</Text>
             <Text style={styles.totalValue}>
-              {formatCurrency(partsTotalDisplay, currencyCode)}
+              {formatCurrency(partsTotalDisplay, currencyCode, currencyFormat)}
             </Text>
           </View>
         )}
         {subDisplay > 0 && (
           <View style={styles.totalRow}>
             <Text style={styles.totalLabel}>{labels.subtotal || 'Subtotal'}</Text>
-            <Text style={styles.totalValue}>{formatCurrency(subDisplay, currencyCode)}</Text>
+            <Text style={styles.totalValue}>{formatCurrency(subDisplay, currencyCode, currencyFormat)}</Text>
           </View>
         )}
         {discDisplay > 0 && (
@@ -582,7 +584,7 @@ export function QuotePDF({
                 : (labels.discount || 'Discount')}
             </Text>
             <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
-              {formatCurrency(-discDisplay, currencyCode)}
+              {formatCurrency(-discDisplay, currencyCode, currencyFormat)}
             </Text>
           </View>
         )}
@@ -593,14 +595,14 @@ export function QuotePDF({
                 ? fillTemplate(labels.tax, { rate: String(data.taxRate) })
                 : `Tax (${data.taxRate}%)`}
             </Text>
-            <Text style={styles.totalValue}>{formatCurrency(tax, currencyCode)}</Text>
+            <Text style={styles.totalValue}>{formatCurrency(tax, currencyCode, currencyFormat)}</Text>
           </View>
         )}
         <View style={styles.totalDivider} />
         <View style={styles.grandTotalRow}>
           <Text style={styles.grandTotalLabel}>{labels.total || 'Total'}</Text>
           <Text style={styles.grandTotalValue}>
-            {formatCurrency(total, currencyCode)}
+            {formatCurrency(total, currencyCode, currencyFormat)}
           </Text>
         </View>
       </View>

+ 3 - 1
src/features/reports/Components/ReportPDF.tsx

@@ -140,6 +140,7 @@ function createReportStyles(primary: string) {
 interface ReportPDFProps {
   dateRange: string;
   currencyCode: string;
+  currencyFormat?: 'symbol' | 'code';
   primaryColor: string;
   organizationName?: string;
   labels: Record<string, string>;
@@ -271,6 +272,7 @@ function PageFooter({ s }: { s: ReportStyles }) {
 export function ReportPDF({
   dateRange,
   currencyCode,
+  currencyFormat = 'symbol',
   primaryColor,
   labels: l,
   revenueData,
@@ -286,7 +288,7 @@ export function ReportPDF({
   vehicleData,
   organizationName,
 }: ReportPDFProps) {
-  const fmt = (n: number) => formatCurrency(n, currencyCode);
+  const fmt = (n: number) => formatCurrency(n, currencyCode, currencyFormat);
   const s = createReportStyles(primaryColor);
 
   const ReportHeader = ({ title, subtitle }: { title: string; subtitle: string }) => (

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

@@ -13,6 +13,7 @@ export const SETTING_KEYS = {
   INVOICE_START_NUMBER: 'workshop.invoiceStartNumber',
   CURRENCY_SYMBOL: 'workshop.currencySymbol',
   CURRENCY_CODE: 'workshop.currencyCode',
+  CURRENCY_FORMAT: 'workshop.currencyFormat',
   INVOICE_BANK_ACCOUNT: 'invoice.bankAccount',
   INVOICE_ORG_NUMBER: 'invoice.orgNumber',
   INVOICE_PAYMENT_TERMS: 'invoice.paymentTerms',

+ 4 - 3
src/features/vehicles/Components/invoice-pdf/InvoicePDF.tsx

@@ -89,6 +89,7 @@ export function InvoicePDF({
   const fontBold = getFontBold(fontFamily)
 
   const cc = invoiceSettings?.currencyCode || 'USD'
+  const cf: 'symbol' | 'code' = invoiceSettings?.currencyFormat === 'code' ? 'code' : 'symbol'
   const vehicleName = `${data.vehicle.year} ${data.vehicle.make} ${data.vehicle.model}`
   const partsSubtotal = data.partItems.reduce((sum, p) => sum + p.total, 0)
   const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
@@ -206,15 +207,15 @@ export function InvoicePDF({
       />
     ),
 
-    parts_table: <PartsTable data={data} currencyCode={cc} styles={styles} labels={labels} />,
+    parts_table: <PartsTable data={data} currencyCode={cc} currencyFormat={cf} styles={styles} labels={labels} />,
 
-    labor_table: <LaborTable data={data} currencyCode={cc} styles={styles} labels={labels} />,
+    labor_table: <LaborTable data={data} currencyCode={cc} currencyFormat={cf} styles={styles} labels={labels} />,
 
     totals: (
       <>
         <Totals
           data={data}
-          currencyCode={cc}
+          currencyCode={cc} currencyFormat={cf}
           primaryColor={primaryColor}
           fontFamily={fontFamily}
           displayTotal={displayTotal}

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

@@ -260,7 +260,7 @@ export function BankAccountSection({
     ? Math.ceil((new Date(dueDate).getTime() - new Date(invoiceDate).getTime()) / 86400000)
     : null
   const paymentTermsText = netDays !== null && netDays > 0
-    ? `Net ${netDays} Days`
+    ? (labels.netDays ? labels.netDays.replace('{days}', String(netDays)) : `Net ${netDays} Days`)
     : invoiceSettings?.paymentTerms || null
   const hasPaymentTerms = !!paymentTermsText
   const hasDueDate = !!dueDate

+ 9 - 8
src/features/vehicles/Components/invoice-pdf/Tables.tsx

@@ -15,11 +15,12 @@ function fillTemplate(template: string, values: Record<string, string>): string
 interface TablesProps {
   data: InvoiceData
   currencyCode: string
+  currencyFormat?: 'symbol' | 'code'
   styles: Record<string, Style>
   labels: Record<string, string>
 }
 
-export function PartsTable({ data, currencyCode, styles, labels }: TablesProps) {
+export function PartsTable({ data, currencyCode, currencyFormat = 'symbol', styles, labels }: TablesProps) {
   if (data.partItems.length === 0) return null
 
   // Universal display: when an inclusive record is rendered, line item prices
@@ -52,10 +53,10 @@ export function PartsTable({ data, currencyCode, styles, labels }: TablesProps)
                 {part.quantity}
               </Text>
               <Text style={{ ...styles.tableCell, width: '18%', textAlign: 'right' }}>
-                {formatCurrency(netUnitPrice, currencyCode)}
+                {formatCurrency(netUnitPrice, currencyCode, currencyFormat)}
               </Text>
               <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
-                {formatCurrency(netLineTotalValue, currencyCode)}
+                {formatCurrency(netLineTotalValue, currencyCode, currencyFormat)}
               </Text>
             </View>
           )
@@ -65,7 +66,7 @@ export function PartsTable({ data, currencyCode, styles, labels }: TablesProps)
   )
 }
 
-export function LaborTable({ data, currencyCode, styles, labels }: TablesProps) {
+export function LaborTable({ data, currencyCode, currencyFormat = 'symbol', styles, labels }: TablesProps) {
   if (data.laborItems.length === 0) return null
 
   // See PartsTable for the rationale on netLineTotal usage.
@@ -96,13 +97,13 @@ export function LaborTable({ data, currencyCode, styles, labels }: TablesProps)
               </Text>
               <Text style={{ ...styles.tableCell, width: '20%', textAlign: 'right' }}>
                 {isService
-                  ? formatCurrency(netRate, currencyCode)
+                  ? formatCurrency(netRate, currencyCode, currencyFormat)
                   : labels.ratePerHour
-                    ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(netRate, currencyCode) })
-                    : `${formatCurrency(netRate, currencyCode)}/hr`}
+                    ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(netRate, currencyCode, currencyFormat) })
+                    : `${formatCurrency(netRate, currencyCode, currencyFormat)}/hr`}
               </Text>
               <Text style={{ ...styles.tableCellBold, width: '20%', textAlign: 'right' }}>
-                {formatCurrency(netLineTotalValue, currencyCode)}
+                {formatCurrency(netLineTotalValue, currencyCode, currencyFormat)}
               </Text>
             </View>
           )

+ 11 - 9
src/features/vehicles/Components/invoice-pdf/Totals.tsx

@@ -15,6 +15,7 @@ function fillTemplate(template: string, values: Record<string, string>): string
 interface TotalsProps {
   data: InvoiceData
   currencyCode: string
+  currencyFormat?: 'symbol' | 'code'
   primaryColor: string
   fontFamily: string
   displayTotal: number
@@ -30,6 +31,7 @@ interface TotalsProps {
 export function Totals({
   data,
   currencyCode,
+  currencyFormat = 'symbol',
   primaryColor,
   fontFamily,
   displayTotal,
@@ -59,19 +61,19 @@ export function Totals({
       {data.partItems.length > 0 && (
         <View style={styles.totalRow}>
           <Text style={styles.totalLabel}>{labels.parts || 'Parts'}</Text>
-          <Text style={styles.totalValue}>{formatCurrency(displayPartsSubtotal, currencyCode)}</Text>
+          <Text style={styles.totalValue}>{formatCurrency(displayPartsSubtotal, currencyCode, currencyFormat)}</Text>
         </View>
       )}
       {data.laborItems.length > 0 && (
         <View style={styles.totalRow}>
           <Text style={styles.totalLabel}>{labels.labor || 'Labor'}</Text>
-          <Text style={styles.totalValue}>{formatCurrency(displayLaborSubtotal, currencyCode)}</Text>
+          <Text style={styles.totalValue}>{formatCurrency(displayLaborSubtotal, currencyCode, currencyFormat)}</Text>
         </View>
       )}
       {displaySubtotal > 0 && (
         <View style={styles.totalRow}>
           <Text style={styles.totalLabel}>{labels.subtotal || 'Subtotal'}</Text>
-          <Text style={styles.totalValue}>{formatCurrency(displaySubtotal, currencyCode)}</Text>
+          <Text style={styles.totalValue}>{formatCurrency(displaySubtotal, currencyCode, currencyFormat)}</Text>
         </View>
       )}
       {displayDiscountAmount > 0 && (
@@ -82,7 +84,7 @@ export function Totals({
               : (labels.discount || 'Discount')}
           </Text>
           <Text style={{ ...styles.totalValue, color: '#dc2626' }}>
-            {formatCurrency(-displayDiscountAmount, currencyCode)}
+            {formatCurrency(-displayDiscountAmount, currencyCode, currencyFormat)}
           </Text>
         </View>
       )}
@@ -93,7 +95,7 @@ export function Totals({
               ? fillTemplate(labels.tax, { rate: String(taxRate) })
               : `Tax (${taxRate}%)`}
           </Text>
-          <Text style={styles.totalValue}>{formatCurrency(data.taxAmount, currencyCode)}</Text>
+          <Text style={styles.totalValue}>{formatCurrency(data.taxAmount, currencyCode, currencyFormat)}</Text>
         </View>
       )}
       <View style={styles.totalDivider} />
@@ -102,7 +104,7 @@ export function Totals({
           <View style={styles.totalRow}>
             <Text style={{ fontSize: 10, fontFamily: fontBold }}>{labels.total || 'Total'}</Text>
             <Text style={{ fontSize: 10, fontFamily: fontBold }}>
-              {formatCurrency(displayTotal, currencyCode)}
+              {formatCurrency(displayTotal, currencyCode, currencyFormat)}
             </Text>
           </View>
           <View style={{ ...styles.totalDivider, marginVertical: 6 }} />
@@ -115,7 +117,7 @@ export function Totals({
                 {p.date} ({p.method})
               </Text>
               <Text style={{ fontSize: 9, color: '#059669' }}>
-                {formatCurrency(-p.amount, currencyCode)}
+                {formatCurrency(-p.amount, currencyCode, currencyFormat)}
               </Text>
             </View>
           ))}
@@ -143,14 +145,14 @@ export function Totals({
                 color: isPaidInFull ? '#059669' : primaryColor,
               }}
             >
-              {isPaidInFull ? '' : formatCurrency(balanceDue, currencyCode)}
+              {isPaidInFull ? '' : formatCurrency(balanceDue, currencyCode, currencyFormat)}
             </Text>
           </View>
         </>
       ) : (
         <View style={styles.grandTotalRow}>
           <Text style={styles.grandTotalLabel}>{labels.total || 'Total'}</Text>
-          <Text style={styles.grandTotalValue}>{formatCurrency(displayTotal, currencyCode)}</Text>
+          <Text style={styles.grandTotalValue}>{formatCurrency(displayTotal, currencyCode, currencyFormat)}</Text>
         </View>
       )}
     </View>

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

@@ -89,6 +89,7 @@ export interface InvoiceSettingsProps {
   showOrgNumber?: boolean
   dueDays?: number
   currencyCode?: string
+  currencyFormat?: 'symbol' | 'code'
   unitSystem?: string
   dateFormat?: string
   timezone?: string

+ 5 - 16
src/features/vehicles/Components/service-history-pdf/ServiceHistoryPDF.tsx

@@ -1,5 +1,5 @@
 import { Document, Page, Text, View, Image } from '@react-pdf/renderer'
-import { formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
+import { formatDateForPdf, DEFAULT_DATE_FORMAT, formatCurrency as formatCurrencyPdf } from '@/lib/format'
 import { createStyles, gray, getFontBold } from '../invoice-pdf/styles'
 import type { WorkshopInfo, InvoiceSettingsProps } from '../invoice-pdf/types'
 
@@ -48,18 +48,6 @@ interface ServiceHistoryPDFProps {
   labels?: Record<string, string>
 }
 
-function formatCurrencyPdf(amount: number, currencyCode: string): string {
-  try {
-    return new Intl.NumberFormat('en-US', {
-      style: 'currency',
-      currency: currencyCode,
-      currencyDisplay: 'narrowSymbol',
-      minimumFractionDigits: 2,
-    }).format(amount)
-  } catch {
-    return `${currencyCode} ${amount.toFixed(2)}`
-  }
-}
 
 export function ServiceHistoryPDF({
   vehicle,
@@ -78,6 +66,7 @@ export function ServiceHistoryPDF({
   const styles = createStyles(primaryColor, fontFamily)
   const fontBold = getFontBold(fontFamily)
   const cc = invoiceSettings?.currencyCode || 'USD'
+  const cf: 'symbol' | 'code' = invoiceSettings?.currencyFormat === 'code' ? 'code' : 'symbol'
   const df = invoiceSettings?.dateFormat || DEFAULT_DATE_FORMAT
   const tz = invoiceSettings?.timezone || undefined
   const unitLabel = invoiceSettings?.unitSystem === 'metric' ? (labels.km || 'km') : (labels.mi || 'mi')
@@ -207,7 +196,7 @@ export function ServiceHistoryPDF({
           </View>
           <View style={{ flex: 1, padding: 8, backgroundColor: '#f3f4f6', borderRadius: 4, alignItems: 'center' }}>
             <Text style={{ fontSize: 16, fontFamily: fontBold, color: primaryColor }}>
-              {formatCurrencyPdf(grandTotal, cc)}
+              {formatCurrencyPdf(grandTotal, cc, cf)}
             </Text>
             <Text style={{ fontSize: 8, color: gray }}>
               {labels.grandTotal || 'Grand Total'}
@@ -252,7 +241,7 @@ export function ServiceHistoryPDF({
                   {record.mileage ? record.mileage.toLocaleString() : '-'}
                 </Text>
                 <Text style={[styles.tableCellBold, { width: '18%', textAlign: 'right' }]}>
-                  {formatCurrencyPdf(displayTotal, cc)}
+                  {formatCurrencyPdf(displayTotal, cc, cf)}
                 </Text>
               </View>
             )
@@ -264,7 +253,7 @@ export function ServiceHistoryPDF({
           <View style={styles.totalDivider} />
           <View style={styles.grandTotalRow}>
             <Text style={styles.grandTotalLabel}>{labels.grandTotal || 'Grand Total'}</Text>
-            <Text style={styles.grandTotalValue}>{formatCurrencyPdf(grandTotal, cc)}</Text>
+            <Text style={styles.grandTotalValue}>{formatCurrencyPdf(grandTotal, cc, cf)}</Text>
           </View>
         </View>
 

+ 3 - 1
src/lib/cron/report-schedules.ts

@@ -362,11 +362,12 @@ export async function processOneSchedule(schedule: {
 
   // Fetch org settings
   const settings = await db.appSetting.findMany({
-    where: { organizationId: schedule.organizationId, key: { in: [SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.INVOICE_PRIMARY_COLOR] } },
+    where: { organizationId: schedule.organizationId, key: { in: [SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.CURRENCY_FORMAT, SETTING_KEYS.INVOICE_PRIMARY_COLOR] } },
   });
   const settingsMap: Record<string, string> = {};
   for (const s of settings) settingsMap[s.key] = s.value;
   const currencyCode = settingsMap[SETTING_KEYS.CURRENCY_CODE] || "USD";
+  const currencyFormat: "symbol" | "code" = settingsMap[SETTING_KEYS.CURRENCY_FORMAT] === "code" ? "code" : "symbol";
   const primaryColor = settingsMap[SETTING_KEYS.INVOICE_PRIMARY_COLOR] || "#d97706";
 
   // Fetch report data for selected sections
@@ -387,6 +388,7 @@ export async function processOneSchedule(schedule: {
   const element = React.createElement(ReportPDF, {
     dateRange: dateRangeStr,
     currencyCode,
+    currencyFormat,
     primaryColor,
     labels: {},
     revenueData,

+ 228 - 129
src/lib/format.ts

@@ -1,47 +1,96 @@
 const CURRENCY_LOCALES: Record<string, string> = {
-  USD: "en-US",
-  EUR: "de-DE",
-  GBP: "en-GB",
-  NOK: "nb-NO",
-  SEK: "sv-SE",
-  DKK: "da-DK",
-  CHF: "de-CH",
-  JPY: "ja-JP",
-  CAD: "en-CA",
-  AUD: "en-AU",
-  NZD: "en-NZ",
-  PLN: "pl-PL",
-  CZK: "cs-CZ",
-  HUF: "hu-HU",
-  BRL: "pt-BR",
-  MXN: "es-MX",
-  INR: "en-IN",
-  CNY: "zh-CN",
-  KRW: "ko-KR",
-  TRY: "tr-TR",
-  ZAR: "en-ZA",
-  RUB: "ru-RU",
-  ISK: "is-IS",
-  THB: "th-TH",
-  SGD: "en-SG",
-  HKD: "zh-HK",
-  TWD: "zh-TW",
-  PHP: "en-PH",
-  ILS: "he-IL",
-  AED: "ar-AE",
-  SAR: "ar-SA",
-  RON: "ro-RO",
-  BGN: "bg-BG",
-  HRK: "hr-HR",
-  UAH: "uk-UA",
-  CLP: "es-CL",
-  COP: "es-CO",
-  ARS: "es-AR",
-  PEN: "es-PE",
-  IDR: "id-ID",
-  MYR: "ms-MY",
-  VND: "vi-VN",
-};
+  USD: 'en-US',
+  EUR: 'de-DE',
+  GBP: 'en-GB',
+  NOK: 'nb-NO',
+  SEK: 'sv-SE',
+  DKK: 'da-DK',
+  CHF: 'de-CH',
+  JPY: 'ja-JP',
+  CAD: 'en-CA',
+  AUD: 'en-AU',
+  NZD: 'en-NZ',
+  PLN: 'pl-PL',
+  CZK: 'cs-CZ',
+  HUF: 'hu-HU',
+  BRL: 'pt-BR',
+  MXN: 'es-MX',
+  INR: 'en-IN',
+  CNY: 'zh-CN',
+  KRW: 'ko-KR',
+  TRY: 'tr-TR',
+  ZAR: 'en-ZA',
+  RUB: 'ru-RU',
+  ISK: 'is-IS',
+  THB: 'th-TH',
+  SGD: 'en-SG',
+  HKD: 'zh-HK',
+  TWD: 'zh-TW',
+  PHP: 'en-PH',
+  ILS: 'en',
+  AED: 'en',
+  SAR: 'en',
+  RON: 'ro-RO',
+  BGN: 'bg-BG',
+  HRK: 'hr-HR',
+  UAH: 'uk-UA',
+  CLP: 'es-CL',
+  COP: 'es-CO',
+  ARS: 'es-AR',
+  PEN: 'es-PE',
+  IDR: 'id-ID',
+  MYR: 'ms-MY',
+  VND: 'vi-VN',
+  LKR: 'en-LK',
+  PKR: 'en-PK',
+  BDT: 'en-BD',
+  NPR: 'en-NP',
+  EGP: 'en-EG',
+  NGN: 'en-NG',
+  KES: 'en-KE',
+  MAD: 'en-MA',
+  GHS: 'en-GH',
+  QAR: 'en',
+  KWD: 'en',
+  BHD: 'en',
+  OMR: 'en',
+  JOD: 'en',
+}
+
+/**
+ * Currencies whose narrow symbol falls outside the WinAnsi encoding used by
+ * the default Helvetica font in @react-pdf/renderer (renders as "?" in PDFs).
+ * For these we fall back to the ISO code in symbol mode.
+ */
+const LATIN_FALLBACK_CURRENCIES = new Set([
+  'JPY',
+  'PLN',
+  'CZK',
+  'INR',
+  'KRW',
+  'TRY',
+  'RUB',
+  'THB',
+  'PHP',
+  'ILS',
+  'BGN',
+  'UAH',
+  'VND',
+  'BDT',
+  'NGN',
+  'GHS',
+])
+
+/**
+ * Symbol overrides for currencies where CLDR's narrowSymbol is ambiguous or
+ * locally non-idiomatic on business documents. Applied only in 'symbol' mode.
+ */
+const CURRENCY_SYMBOL_OVERRIDES: Record<string, string> = {
+  CNY: 'RMB', // disambiguates from JPY ¥; matches Chinese invoice convention
+}
+
+export type CurrencyFormat = 'symbol' | 'code'
+export const DEFAULT_CURRENCY_FORMAT: CurrencyFormat = 'symbol'
 
 /**
  * Format a number as currency using the correct locale for the given ISO 4217 currency code.
@@ -53,20 +102,47 @@ const CURRENCY_LOCALES: Record<string, string> = {
  */
 export function formatCurrency(
   amount: number,
-  currencyCode: string = "USD",
+  currencyCode: string = 'USD',
+  format: CurrencyFormat = DEFAULT_CURRENCY_FORMAT
 ): string {
-  const locale = CURRENCY_LOCALES[currencyCode] || "en-US";
+  const locale = CURRENCY_LOCALES[currencyCode] || 'en-US'
+  // 'code' mode: always render the ISO code.
+  // 'symbol' mode: prefer narrowSymbol, but fall back to code for currencies
+  // whose symbol isn't WinAnsi-safe in the default PDF font.
+  const useCode = format === 'code' || LATIN_FALLBACK_CURRENCIES.has(currencyCode)
+  const display = useCode ? 'code' : 'narrowSymbol'
   try {
-    return new Intl.NumberFormat(locale, {
-      style: "currency",
+    let result = new Intl.NumberFormat(locale, {
+      style: 'currency',
       currency: currencyCode,
-      currencyDisplay: "narrowSymbol",
+      currencyDisplay: display,
       minimumFractionDigits: 2,
       maximumFractionDigits: 2,
-    }).format(amount);
+    }).format(amount)
+    if (format === 'symbol' && CURRENCY_SYMBOL_OVERRIDES[currencyCode]) {
+      // Replace CLDR's currency token with our override (e.g. ¥ → RMB).
+      const parts = new Intl.NumberFormat(locale, {
+        style: 'currency',
+        currency: currencyCode,
+        currencyDisplay: 'narrowSymbol',
+        minimumFractionDigits: 2,
+        maximumFractionDigits: 2,
+      }).formatToParts(amount)
+      const sym = parts.find((p) => p.type === 'currency')?.value
+      if (sym) {
+        const override = CURRENCY_SYMBOL_OVERRIDES[currencyCode]
+        // If the original symbol was glued to a digit/letter (e.g. "¥1,234.56"),
+        // a multi-character override needs a space for readability.
+        result = result.replace(sym, override).replace(
+          new RegExp(`(${override.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})(?=\\d)`),
+          '$1 '
+        )
+      }
+    }
+    return result
   } catch {
     // Fallback if the currency code is not recognized by Intl
-    return `${currencyCode} ${amount.toFixed(2)}`;
+    return `${currencyCode} ${amount.toFixed(2)}`
   }
 }
 
@@ -79,16 +155,22 @@ export function formatCurrency(
  *   getCurrencySymbol("USD") → "$"
  *   getCurrencySymbol("EUR") → "€"
  */
-export function getCurrencySymbol(currencyCode: string = "USD"): string {
+export function getCurrencySymbol(
+  currencyCode: string = 'USD',
+  format: CurrencyFormat = DEFAULT_CURRENCY_FORMAT
+): string {
+  if (format === 'code') return currencyCode
+  if (CURRENCY_SYMBOL_OVERRIDES[currencyCode]) return CURRENCY_SYMBOL_OVERRIDES[currencyCode]
+  if (LATIN_FALLBACK_CURRENCIES.has(currencyCode)) return currencyCode
   try {
-    const parts = new Intl.NumberFormat("en", {
-      style: "currency",
+    const parts = new Intl.NumberFormat('en', {
+      style: 'currency',
       currency: currencyCode,
-      currencyDisplay: "narrowSymbol",
-    }).formatToParts(0);
-    return parts.find((p) => p.type === "currency")?.value || currencyCode;
+      currencyDisplay: 'narrowSymbol',
+    }).formatToParts(0)
+    return parts.find((p) => p.type === 'currency')?.value || currencyCode
   } catch {
-    return currencyCode;
+    return currencyCode
   }
 }
 
@@ -99,38 +181,38 @@ export function getCurrencySymbol(currencyCode: string = "USD"): string {
  */
 function toTimezone(date: Date, timezone: string): Date {
   try {
-    const parts = new Intl.DateTimeFormat("en-US", {
+    const parts = new Intl.DateTimeFormat('en-US', {
       timeZone: timezone,
-      year: "numeric",
-      month: "2-digit",
-      day: "2-digit",
-      hour: "2-digit",
-      minute: "2-digit",
-      second: "2-digit",
+      year: 'numeric',
+      month: '2-digit',
+      day: '2-digit',
+      hour: '2-digit',
+      minute: '2-digit',
+      second: '2-digit',
       hour12: false,
-    }).formatToParts(date);
+    }).formatToParts(date)
 
     const get = (type: Intl.DateTimeFormatPartTypes) =>
-      Number(parts.find((p) => p.type === type)?.value || 0);
+      Number(parts.find((p) => p.type === type)?.value || 0)
 
     return new Date(
-      get("year"),
-      get("month") - 1,
-      get("day"),
-      get("hour") === 24 ? 0 : get("hour"),
-      get("minute"),
-      get("second"),
-    );
+      get('year'),
+      get('month') - 1,
+      get('day'),
+      get('hour') === 24 ? 0 : get('hour'),
+      get('minute'),
+      get('second')
+    )
   } catch {
-    return date;
+    return date
   }
 }
 
 /**
  * Default date format matching the most common existing pattern.
  */
-export const DEFAULT_DATE_FORMAT = "MMM d, yyyy";
-export const DEFAULT_TIME_FORMAT = "12h" as const;
+export const DEFAULT_DATE_FORMAT = 'MMM d, yyyy'
+export const DEFAULT_TIME_FORMAT = '12h' as const
 
 /**
  * Simple date-fns-style format function that handles common tokens.
@@ -138,43 +220,63 @@ export const DEFAULT_TIME_FORMAT = "12h" as const;
  */
 function formatWithPattern(date: Date, pattern: string): string {
   const months = [
-    "January", "February", "March", "April", "May", "June",
-    "July", "August", "September", "October", "November", "December",
-  ];
+    'January',
+    'February',
+    'March',
+    'April',
+    'May',
+    'June',
+    'July',
+    'August',
+    'September',
+    'October',
+    'November',
+    'December',
+  ]
   const monthsShort = [
-    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
-    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
-  ];
-
-  const y = date.getFullYear();
-  const M = date.getMonth();
-  const d = date.getDate();
-  const H = date.getHours();
-  const m = date.getMinutes();
-  const s = date.getSeconds();
-  const h12 = H % 12 || 12;
-  const ampm = H < 12 ? "AM" : "PM";
+    'Jan',
+    'Feb',
+    'Mar',
+    'Apr',
+    'May',
+    'Jun',
+    'Jul',
+    'Aug',
+    'Sep',
+    'Oct',
+    'Nov',
+    'Dec',
+  ]
+
+  const y = date.getFullYear()
+  const M = date.getMonth()
+  const d = date.getDate()
+  const H = date.getHours()
+  const m = date.getMinutes()
+  const s = date.getSeconds()
+  const h12 = H % 12 || 12
+  const ampm = H < 12 ? 'AM' : 'PM'
 
   // Use placeholders for month names to avoid collisions with later replacements
   // (e.g. "a" in "May" being replaced by the AM/PM token)
-  const MONTH_LONG = "\x01";
-  const MONTH_SHORT = "\x02";
+  const MONTH_LONG = '\x01'
+  const MONTH_SHORT = '\x02'
 
   return pattern
-    .replace("yyyy", String(y))
-    .replace("yy", String(y).slice(-2))
-    .replace("MMMM", MONTH_LONG)
-    .replace("MMM", MONTH_SHORT)
-    .replace("MM", String(M + 1).padStart(2, "0"))
-    .replace(/\bdd\b/, String(d).padStart(2, "0"))
+    .replace('yyyy', String(y))
+    .replace('yy', String(y).slice(-2))
+    .replace('MMMM', MONTH_LONG)
+    .replace('MMM', MONTH_SHORT)
+    .replace('MM', String(M + 1).padStart(2, '0'))
+    .replace(/\bdd\b/, String(d).padStart(2, '0'))
     .replace(/\bd\b/, String(d))
-    .replace("HH", String(H).padStart(2, "0"))
-    .replace("hh", String(h12).padStart(2, "0"))
-    .replace("mm", String(m).padStart(2, "0"))
-    .replace("ss", String(s).padStart(2, "0"))
-    .replace("a", ampm)
+    .replace('HH', String(H).padStart(2, '0'))
+    .replace('hh', String(h12).padStart(2, '0'))
+    .replace('mm', String(m).padStart(2, '0'))
+    .replace('ss', String(s).padStart(2, '0'))
+    .replace('a', ampm)
     .replace(MONTH_LONG, months[M])
-    .replace(MONTH_SHORT, monthsShort[M]);
+    .replace(MONTH_SHORT, monthsShort[M])
 }
 
 /**
@@ -183,11 +285,11 @@ function formatWithPattern(date: Date, pattern: string): string {
 export function formatDate(
   date: Date | string,
   dateFormat: string = DEFAULT_DATE_FORMAT,
-  timezone?: string,
+  timezone?: string
 ): string {
-  let d = typeof date === "string" ? new Date(date) : date;
-  if (timezone) d = toTimezone(d, timezone);
-  return formatWithPattern(d, dateFormat);
+  let d = typeof date === 'string' ? new Date(date) : date
+  if (timezone) d = toTimezone(d, timezone)
+  return formatWithPattern(d, dateFormat)
 }
 
 /**
@@ -196,16 +298,15 @@ export function formatDate(
 export function formatDateTime(
   date: Date | string,
   dateFormat: string = DEFAULT_DATE_FORMAT,
-  timeFormat: "12h" | "24h" = "12h",
-  timezone?: string,
+  timeFormat: '12h' | '24h' = '12h',
+  timezone?: string
 ): string {
-  let d = typeof date === "string" ? new Date(date) : date;
-  if (timezone) d = toTimezone(d, timezone);
-  const datePart = formatWithPattern(d, dateFormat);
-  const timePart = timeFormat === "24h"
-    ? formatWithPattern(d, "HH:mm")
-    : formatWithPattern(d, "hh:mm a");
-  return `${datePart} ${timePart}`;
+  let d = typeof date === 'string' ? new Date(date) : date
+  if (timezone) d = toTimezone(d, timezone)
+  const datePart = formatWithPattern(d, dateFormat)
+  const timePart =
+    timeFormat === '24h' ? formatWithPattern(d, 'HH:mm') : formatWithPattern(d, 'hh:mm a')
+  return `${datePart} ${timePart}`
 }
 
 /**
@@ -213,14 +314,12 @@ export function formatDateTime(
  */
 export function formatTime(
   date: Date | string,
-  timeFormat: "12h" | "24h" = "12h",
-  timezone?: string,
+  timeFormat: '12h' | '24h' = '12h',
+  timezone?: string
 ): string {
-  let d = typeof date === "string" ? new Date(date) : date;
-  if (timezone) d = toTimezone(d, timezone);
-  return timeFormat === "24h"
-    ? formatWithPattern(d, "HH:mm")
-    : formatWithPattern(d, "hh:mm a");
+  let d = typeof date === 'string' ? new Date(date) : date
+  if (timezone) d = toTimezone(d, timezone)
+  return timeFormat === '24h' ? formatWithPattern(d, 'HH:mm') : formatWithPattern(d, 'hh:mm a')
 }
 
 /**
@@ -230,11 +329,11 @@ export function formatTime(
 export function formatDateForPdf(
   date: Date | string,
   dateFormat: string = DEFAULT_DATE_FORMAT,
-  timezone?: string,
+  timezone?: string
 ): string {
-  let d = typeof date === "string" ? new Date(date) : date;
+  let d = typeof date === 'string' ? new Date(date) : date
   if (timezone) {
-    d = toTimezone(d, timezone);
+    d = toTimezone(d, timezone)
   }
-  return formatWithPattern(d, dateFormat);
+  return formatWithPattern(d, dateFormat)
 }

+ 6 - 0
src/lib/get-layout-data.ts

@@ -17,6 +17,8 @@ type AuthResult =
       timeFormat: string | undefined;
       timezone: string | undefined;
       serviceType: string | undefined;
+      currencyCode: string | undefined;
+      currencyFormat: string | undefined;
       organizations: { id: string; name: string; role: string }[];
     };
 
@@ -51,6 +53,8 @@ export async function getLayoutData(): Promise<AuthResult> {
                 SETTING_KEYS.TIME_FORMAT,
                 SETTING_KEYS.TIMEZONE,
                 SETTING_KEYS.SERVICE_TYPE,
+                SETTING_KEYS.CURRENCY_CODE,
+                SETTING_KEYS.CURRENCY_FORMAT,
               ],
             },
           },
@@ -80,6 +84,8 @@ export async function getLayoutData(): Promise<AuthResult> {
     timeFormat: orgMap.get(SETTING_KEYS.TIME_FORMAT) || undefined,
     timezone: orgMap.get(SETTING_KEYS.TIMEZONE) || undefined,
     serviceType: orgMap.get(SETTING_KEYS.SERVICE_TYPE) || undefined,
+    currencyCode: orgMap.get(SETTING_KEYS.CURRENCY_CODE) || undefined,
+    currencyFormat: orgMap.get(SETTING_KEYS.CURRENCY_FORMAT) || undefined,
     organizations: memberships.map((m) => ({
       id: m.organization.id,
       name: m.organization.name,