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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
fd20b07e83
commit
52fbec3214
@@ -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 (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
pattern="[0-9]*[.,]?[0-9]*"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import OmsorgButton from "../../components/ui/OmsorgButton";
|
import OmsorgButton from "../../components/ui/OmsorgButton";
|
||||||
|
import DecimalInput from "../../components/ui/DecimalInput";
|
||||||
import { useValueListItems } from "../../app/useValueListItems";
|
import { useValueListItems } from "../../app/useValueListItems";
|
||||||
|
import { parseGermanDecimal } from "../../utils/germanDecimal";
|
||||||
|
|
||||||
export const emptyContractForm = {
|
export const emptyContractForm = {
|
||||||
contractType: "",
|
contractType: "",
|
||||||
@@ -36,8 +38,8 @@ export function contractFormToPayload(form, { employeeId, includeStatus = false
|
|||||||
facilityId: null,
|
facilityId: null,
|
||||||
startDate: form.startDate,
|
startDate: form.startDate,
|
||||||
endDate: form.endDate || null,
|
endDate: form.endDate || null,
|
||||||
weeklyHours: form.weeklyHours === "" ? null : Number(form.weeklyHours),
|
weeklyHours: parseGermanDecimal(form.weeklyHours),
|
||||||
hourlyWage: form.hourlyWage === "" ? null : Number(form.hourlyWage),
|
hourlyWage: parseGermanDecimal(form.hourlyWage),
|
||||||
allowancesDescription: form.allowancesDescription.trim() || null,
|
allowancesDescription: form.allowancesDescription.trim() || null,
|
||||||
overtimeRules: form.overtimeRules.trim() || null,
|
overtimeRules: form.overtimeRules.trim() || null,
|
||||||
vacationDaysPerYear: form.vacationDaysPerYear === "" ? null : Number(form.vacationDaysPerYear),
|
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 });
|
return (event) => onChange({ ...form, [field]: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDecimalField(field) {
|
||||||
|
return (rawValue) => onChange({ ...form, [field]: rawValue });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
@@ -101,12 +107,12 @@ export default function ContractForm({ form, onChange, includeStatus = false })
|
|||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Arbeitszeit (Std./Woche)</span>
|
<span>Arbeitszeit (Std./Woche)</span>
|
||||||
<input type="number" min="0" step="0.5" value={form.weeklyHours} onChange={updateField("weeklyHours")} />
|
<DecimalInput value={form.weeklyHours} onChange={updateDecimalField("weeklyHours")} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Stundenlohn (€)</span>
|
<span>Stundenlohn (€)</span>
|
||||||
<input type="number" min="0" step="0.01" value={form.hourlyWage} onChange={updateField("hourlyWage")} />
|
<DecimalInput value={form.hourlyWage} onChange={updateDecimalField("hourlyWage")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Pencil, Trash2 } from "lucide-react";
|
|||||||
import OmsorgCard from "../../components/OmsorgCard";
|
import OmsorgCard from "../../components/OmsorgCard";
|
||||||
import OmsorgButton from "../../components/ui/OmsorgButton";
|
import OmsorgButton from "../../components/ui/OmsorgButton";
|
||||||
import { useAuth } from "../../app/AuthContext";
|
import { useAuth } from "../../app/AuthContext";
|
||||||
|
import { formatGermanDecimal } from "../../utils/germanDecimal";
|
||||||
import EditFacilityDialog from "./EditFacilityDialog";
|
import EditFacilityDialog from "./EditFacilityDialog";
|
||||||
import FacilityContactsList from "./FacilityContactsList";
|
import FacilityContactsList from "./FacilityContactsList";
|
||||||
import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
|
import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
|
||||||
@@ -126,14 +127,14 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Verrechnungssatz</strong>
|
<strong>Verrechnungssatz</strong>
|
||||||
<p>{facility.billingRate != null ? `${facility.billingRate} EUR/Std.` : "—"}</p>
|
<p>{facility.billingRate != null ? `${formatGermanDecimal(facility.billingRate)} EUR/Std.` : "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Zuschläge</strong>
|
<strong>Zuschläge</strong>
|
||||||
<p>
|
<p>
|
||||||
Nacht {facility.nightSurchargePercent ?? "—"}% · Sa {facility.saturdaySurchargePercent ?? "—"}% ·
|
Nacht {facility.nightSurchargePercent != null ? formatGermanDecimal(facility.nightSurchargePercent) : "—"}% · Sa {facility.saturdaySurchargePercent != null ? formatGermanDecimal(facility.saturdaySurchargePercent) : "—"}% ·
|
||||||
So {facility.sundaySurchargePercent ?? "—"}% · Feiertag {facility.holidaySurchargePercent ?? "—"}%
|
So {facility.sundaySurchargePercent != null ? formatGermanDecimal(facility.sundaySurchargePercent) : "—"}% · Feiertag {facility.holidaySurchargePercent != null ? formatGermanDecimal(facility.holidaySurchargePercent) : "—"}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -142,22 +143,22 @@ export default function FacilityDetailPanel({ facility, onUpdated }) {
|
|||||||
<p>
|
<p>
|
||||||
{facility.travelCostMode === "ProKilometer"
|
{facility.travelCostMode === "ProKilometer"
|
||||||
? facility.travelCostPerKm != null
|
? facility.travelCostPerKm != null
|
||||||
? `${facility.travelCostPerKm} EUR/km`
|
? `${formatGermanDecimal(facility.travelCostPerKm)} EUR/km`
|
||||||
: "—"
|
: "—"
|
||||||
: facility.travelCostRate != null
|
: facility.travelCostRate != null
|
||||||
? `${facility.travelCostRate} EUR (Pauschale je Einsatz)`
|
? `${formatGermanDecimal(facility.travelCostRate)} EUR (Pauschale je Einsatz)`
|
||||||
: "—"}
|
: "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Verpflegungsmehraufwand</strong>
|
<strong>Verpflegungsmehraufwand</strong>
|
||||||
<p>{facility.mealAllowanceRate != null ? `${facility.mealAllowanceRate} EUR (Pauschale je Einsatz)` : "—"}</p>
|
<p>{facility.mealAllowanceRate != null ? `${formatGermanDecimal(facility.mealAllowanceRate)} EUR (Pauschale je Einsatz)` : "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Mindeststunden</strong>
|
<strong>Mindeststunden</strong>
|
||||||
<p>{facility.minimumHours ?? "—"}</p>
|
<p>{facility.minimumHours != null ? formatGermanDecimal(facility.minimumHours) : "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import OmsorgButton from "../../components/ui/OmsorgButton";
|
import OmsorgButton from "../../components/ui/OmsorgButton";
|
||||||
|
import DecimalInput from "../../components/ui/DecimalInput";
|
||||||
import { useValueListItems } from "../../app/useValueListItems";
|
import { useValueListItems } from "../../app/useValueListItems";
|
||||||
|
import { parseGermanDecimal } from "../../utils/germanDecimal";
|
||||||
|
|
||||||
export const emptyFacilityForm = {
|
export const emptyFacilityForm = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -79,16 +81,16 @@ export function facilityFormToPayload(form, { includeCrmStatus = false, followUp
|
|||||||
if (includeCrmStatus) {
|
if (includeCrmStatus) {
|
||||||
payload.crmStatus = form.crmStatus;
|
payload.crmStatus = form.crmStatus;
|
||||||
payload.followUpDays = followUpDays;
|
payload.followUpDays = followUpDays;
|
||||||
payload.billingRate = form.billingRate === "" ? null : Number(form.billingRate);
|
payload.billingRate = parseGermanDecimal(form.billingRate);
|
||||||
payload.nightSurchargePercent = form.nightSurchargePercent === "" ? null : Number(form.nightSurchargePercent);
|
payload.nightSurchargePercent = parseGermanDecimal(form.nightSurchargePercent);
|
||||||
payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent);
|
payload.saturdaySurchargePercent = parseGermanDecimal(form.saturdaySurchargePercent);
|
||||||
payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent);
|
payload.sundaySurchargePercent = parseGermanDecimal(form.sundaySurchargePercent);
|
||||||
payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent);
|
payload.holidaySurchargePercent = parseGermanDecimal(form.holidaySurchargePercent);
|
||||||
payload.travelCostMode = form.travelCostMode || "Pauschale";
|
payload.travelCostMode = form.travelCostMode || "Pauschale";
|
||||||
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
|
payload.travelCostRate = parseGermanDecimal(form.travelCostRate);
|
||||||
payload.travelCostPerKm = form.travelCostPerKm === "" ? null : Number(form.travelCostPerKm);
|
payload.travelCostPerKm = parseGermanDecimal(form.travelCostPerKm);
|
||||||
payload.mealAllowanceRate = form.mealAllowanceRate === "" ? null : Number(form.mealAllowanceRate);
|
payload.mealAllowanceRate = parseGermanDecimal(form.mealAllowanceRate);
|
||||||
payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours);
|
payload.minimumHours = parseGermanDecimal(form.minimumHours);
|
||||||
payload.billingInterval = form.billingInterval || null;
|
payload.billingInterval = form.billingInterval || null;
|
||||||
payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays);
|
payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays);
|
||||||
payload.individualAgreements = form.individualAgreements.trim() || null;
|
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 });
|
return (event) => onChange({ ...form, [field]: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDecimalField(field) {
|
||||||
|
return (rawValue) => onChange({ ...form, [field]: rawValue });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
@@ -226,30 +232,30 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
|||||||
|
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Verrechnungssatz (EUR/Std.)</span>
|
<span>Verrechnungssatz (EUR/Std.)</span>
|
||||||
<input type="number" min="0" step="0.01" value={form.billingRate} onChange={updateField("billingRate")} />
|
<DecimalInput value={form.billingRate} onChange={updateDecimalField("billingRate")} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Nachtzuschlag (%)</span>
|
<span>Nachtzuschlag (%)</span>
|
||||||
<input type="number" min="0" step="0.1" value={form.nightSurchargePercent} onChange={updateField("nightSurchargePercent")} />
|
<DecimalInput value={form.nightSurchargePercent} onChange={updateDecimalField("nightSurchargePercent")} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Samstagszuschlag (%)</span>
|
<span>Samstagszuschlag (%)</span>
|
||||||
<input type="number" min="0" step="0.1" value={form.saturdaySurchargePercent} onChange={updateField("saturdaySurchargePercent")} />
|
<DecimalInput value={form.saturdaySurchargePercent} onChange={updateDecimalField("saturdaySurchargePercent")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Sonntagszuschlag (%)</span>
|
<span>Sonntagszuschlag (%)</span>
|
||||||
<input type="number" min="0" step="0.1" value={form.sundaySurchargePercent} onChange={updateField("sundaySurchargePercent")} />
|
<DecimalInput value={form.sundaySurchargePercent} onChange={updateDecimalField("sundaySurchargePercent")} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Feiertagszuschlag (%)</span>
|
<span>Feiertagszuschlag (%)</span>
|
||||||
<input type="number" min="0" step="0.1" value={form.holidaySurchargePercent} onChange={updateField("holidaySurchargePercent")} />
|
<DecimalInput value={form.holidaySurchargePercent} onChange={updateDecimalField("holidaySurchargePercent")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -264,7 +270,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
|||||||
|
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Mindeststunden je Einsatz</span>
|
<span>Mindeststunden je Einsatz</span>
|
||||||
<input type="number" min="0" step="0.5" value={form.minimumHours} onChange={updateField("minimumHours")} />
|
<DecimalInput value={form.minimumHours} onChange={updateDecimalField("minimumHours")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,12 +278,12 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
|||||||
{form.travelCostMode === "ProKilometer" ? (
|
{form.travelCostMode === "ProKilometer" ? (
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Fahrtkosten (EUR/km)</span>
|
<span>Fahrtkosten (EUR/km)</span>
|
||||||
<input type="number" min="0" step="0.01" value={form.travelCostPerKm} onChange={updateField("travelCostPerKm")} />
|
<DecimalInput value={form.travelCostPerKm} onChange={updateDecimalField("travelCostPerKm")} />
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Fahrtkosten (EUR, Pauschale je Einsatz)</span>
|
<span>Fahrtkosten (EUR, Pauschale je Einsatz)</span>
|
||||||
<input type="number" min="0" step="0.01" value={form.travelCostRate} onChange={updateField("travelCostRate")} />
|
<DecimalInput value={form.travelCostRate} onChange={updateDecimalField("travelCostRate")} />
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +291,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false,
|
|||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
|
<span>Verpflegungsmehraufwand (EUR, Pauschale je Einsatz)</span>
|
||||||
<input type="number" min="0" step="0.01" value={form.mealAllowanceRate} onChange={updateField("mealAllowanceRate")} />
|
<DecimalInput value={form.mealAllowanceRate} onChange={updateDecimalField("mealAllowanceRate")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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(".", ",");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user