From 52fbec32149c682ff4efe8dfc3f18bef2ee8cde0 Mon Sep 17 00:00:00 2001 From: Felix Kemmler Date: Mon, 10 Aug 2026 22:16:20 +0200 Subject: [PATCH] Add German comma decimal input for currency/decimal fields - New utility module `germanDecimal.js` with parseGermanDecimal(), sanitizeDecimalInput(), and formatGermanDecimal() - New DecimalInput component replacing type="number" inputs - Enables users to enter comma as decimal separator (12,50) - Applied to: hourlyWage, weeklyHours (ContractForm) and billingRate, travelCostRate, travelCostPerKm, mealAllowanceRate, minimumHours, and percent surcharges (FacilityForm) - Display formatting updated in FacilityDetailPanel to show comma decimals Both comma and dot are accepted as decimal separators. Backend decimal types remain unchanged (already decimal?). Co-Authored-By: Claude Haiku 4.5 --- omsorgapp/src/components/ui/DecimalInput.jsx | 36 ++++++++++++++++ .../src/modules/employees/ContractForm.jsx | 14 +++++-- .../facilities/FacilityDetailPanel.jsx | 15 +++---- .../src/modules/facilities/FacilityForm.jsx | 42 +++++++++++-------- omsorgapp/src/utils/germanDecimal.js | 32 ++++++++++++++ 5 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 omsorgapp/src/components/ui/DecimalInput.jsx create mode 100644 omsorgapp/src/utils/germanDecimal.js diff --git a/omsorgapp/src/components/ui/DecimalInput.jsx b/omsorgapp/src/components/ui/DecimalInput.jsx new file mode 100644 index 0000000..014c881 --- /dev/null +++ b/omsorgapp/src/components/ui/DecimalInput.jsx @@ -0,0 +1,36 @@ +/** + * ========================================================== + * OMSORG BUSINESS CONTROLS PRO + * Component: DecimalInput + * Sprint: 0.2.0 + * ========================================================== + */ + +import { sanitizeDecimalInput } from "../../utils/germanDecimal"; + +export default function DecimalInput({ + value, + onChange, + placeholder, + required = false, + disabled = false, + id, +}) { + function handleChange(event) { + onChange(sanitizeDecimalInput(event.target.value)); + } + + return ( + + ); +} diff --git a/omsorgapp/src/modules/employees/ContractForm.jsx b/omsorgapp/src/modules/employees/ContractForm.jsx index 459b1d2..94a548e 100644 --- a/omsorgapp/src/modules/employees/ContractForm.jsx +++ b/omsorgapp/src/modules/employees/ContractForm.jsx @@ -1,5 +1,7 @@ import OmsorgButton from "../../components/ui/OmsorgButton"; +import DecimalInput from "../../components/ui/DecimalInput"; import { useValueListItems } from "../../app/useValueListItems"; +import { parseGermanDecimal } from "../../utils/germanDecimal"; export const emptyContractForm = { contractType: "", @@ -36,8 +38,8 @@ export function contractFormToPayload(form, { employeeId, includeStatus = false facilityId: null, startDate: form.startDate, endDate: form.endDate || null, - weeklyHours: form.weeklyHours === "" ? null : Number(form.weeklyHours), - hourlyWage: form.hourlyWage === "" ? null : Number(form.hourlyWage), + weeklyHours: parseGermanDecimal(form.weeklyHours), + hourlyWage: parseGermanDecimal(form.hourlyWage), allowancesDescription: form.allowancesDescription.trim() || null, overtimeRules: form.overtimeRules.trim() || null, vacationDaysPerYear: form.vacationDaysPerYear === "" ? null : Number(form.vacationDaysPerYear), @@ -59,6 +61,10 @@ export default function ContractForm({ form, onChange, includeStatus = false }) return (event) => onChange({ ...form, [field]: event.target.value }); } + function updateDecimalField(field) { + return (rawValue) => onChange({ ...form, [field]: rawValue }); + } + return (
Verpflegungsmehraufwand -

{facility.mealAllowanceRate != null ? `${facility.mealAllowanceRate} EUR (Pauschale je Einsatz)` : "—"}

+

{facility.mealAllowanceRate != null ? `${formatGermanDecimal(facility.mealAllowanceRate)} EUR (Pauschale je Einsatz)` : "—"}

Mindeststunden -

{facility.minimumHours ?? "—"}

+

{facility.minimumHours != null ? formatGermanDecimal(facility.minimumHours) : "—"}

diff --git a/omsorgapp/src/modules/facilities/FacilityForm.jsx b/omsorgapp/src/modules/facilities/FacilityForm.jsx index bff6ff9..0896a2c 100644 --- a/omsorgapp/src/modules/facilities/FacilityForm.jsx +++ b/omsorgapp/src/modules/facilities/FacilityForm.jsx @@ -1,7 +1,9 @@ import { useEffect, useState } from "react"; import OmsorgButton from "../../components/ui/OmsorgButton"; +import DecimalInput from "../../components/ui/DecimalInput"; import { useValueListItems } from "../../app/useValueListItems"; +import { parseGermanDecimal } from "../../utils/germanDecimal"; export const emptyFacilityForm = { name: "", @@ -79,16 +81,16 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp if (includeCrmStatus) { payload.crmStatus = form.crmStatus; payload.followUpDays = followUpDays; - payload.billingRate = form.billingRate === "" ? null : Number(form.billingRate); - payload.nightSurchargePercent = form.nightSurchargePercent === "" ? null : Number(form.nightSurchargePercent); - payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent); - payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent); - payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent); + payload.billingRate = parseGermanDecimal(form.billingRate); + payload.nightSurchargePercent = parseGermanDecimal(form.nightSurchargePercent); + payload.saturdaySurchargePercent = parseGermanDecimal(form.saturdaySurchargePercent); + payload.sundaySurchargePercent = parseGermanDecimal(form.sundaySurchargePercent); + payload.holidaySurchargePercent = parseGermanDecimal(form.holidaySurchargePercent); payload.travelCostMode = form.travelCostMode || "Pauschale"; - payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate); - payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm); - payload.mealAllowanceRate = form.mealAllowanceRate === "" ? null : Number(form.mealAllowanceRate); - payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours); + payload.travelCostRate = parseGermanDecimal(form.travelCostRate); + payload.travelCostPerKm = parseGermanDecimal(form.travelCostPerKm); + payload.mealAllowanceRate = parseGermanDecimal(form.mealAllowanceRate); + payload.minimumHours = parseGermanDecimal(form.minimumHours); payload.billingInterval = form.billingInterval || null; payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays); payload.individualAgreements = form.individualAgreements.trim() || null; @@ -131,6 +133,10 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false, return (event) => onChange({ ...form, [field]: event.target.value }); } + function updateDecimalField(field) { + return (rawValue) => onChange({ ...form, [field]: rawValue }); + } + return (
@@ -272,12 +278,12 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false, {form.travelCostMode === "ProKilometer" ? ( ) : ( )}
@@ -285,7 +291,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
diff --git a/omsorgapp/src/utils/germanDecimal.js b/omsorgapp/src/utils/germanDecimal.js new file mode 100644 index 0000000..3f8e420 --- /dev/null +++ b/omsorgapp/src/utils/germanDecimal.js @@ -0,0 +1,32 @@ +export function sanitizeDecimalInput(rawValue) { + if (!rawValue) return ""; + const cleaned = rawValue.replace(/[^\d,.]/g, ""); + let separatorIdx = -1; + let result = ""; + for (let i = 0; i < cleaned.length; i++) { + if (cleaned[i] === "," || cleaned[i] === ".") { + if (separatorIdx === -1) { + separatorIdx = result.length; + result += cleaned[i]; + } + } else { + result += cleaned[i]; + } + } + return result; +} + +export function parseGermanDecimal(value) { + if (value === "" || value == null) return null; + const trimmed = String(value).trim(); + if (!trimmed) return null; + const normalized = trimmed.replace(",", "."); + if (!/^\d+(\.\d+)?$/.test(normalized)) return null; + return Number(normalized); +} + +export function formatGermanDecimal(value, { fractionDigits } = {}) { + if (value == null) return ""; + const str = fractionDigits != null ? Number(value).toFixed(fractionDigits) : String(value); + return str.replace(".", ","); +}