From bd0b7746abb012f99eef10a452eb648db53bb21e Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 8 Apr 2026 18:49:10 +0200 Subject: [PATCH] fix: support pasting numbers with comma decimal separator in budget and bookings Handle European number formats (e.g. 1.150,32) by detecting the last separator as decimal and stripping thousand separators. Applied to budget inline edit cells, add item row, and reservation price field. Fixes #498 --- client/src/components/Budget/BudgetPanel.tsx | 23 ++++++++++++++++++- .../components/Planner/ReservationModal.tsx | 3 ++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/src/components/Budget/BudgetPanel.tsx b/client/src/components/Budget/BudgetPanel.tsx index e1f117bc..f85f4079 100644 --- a/client/src/components/Budget/BudgetPanel.tsx +++ b/client/src/components/Budget/BudgetPanel.tsx @@ -75,9 +75,29 @@ function InlineEditCell({ value, onSave, type = 'text', style = {}, placeholder if (v !== value) onSave(v) } + const handlePaste = (e) => { + if (type !== 'number') return + e.preventDefault() + let text = e.clipboardData.getData('text').trim() + // Strip everything except digits, dots, commas, minus + text = text.replace(/[^\d.,-]/g, '') + // Remove all thousand separators (dots or commas before 3-digit groups), keep last separator as decimal + const lastComma = text.lastIndexOf(',') + const lastDot = text.lastIndexOf('.') + const decimalPos = Math.max(lastComma, lastDot) + if (decimalPos > -1) { + const intPart = text.substring(0, decimalPos).replace(/[.,]/g, '') + const decPart = text.substring(decimalPos + 1) + text = intPart + '.' + decPart + } else { + text = text.replace(/[.,]/g, '') + } + setEditValue(text) + } + if (editing) { return setEditValue(e.target.value)} onBlur={save} + onChange={e => setEditValue(e.target.value)} onBlur={save} onPaste={handlePaste} onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setEditValue(value ?? ''); setEditing(false) } }} style={{ width: '100%', border: '1px solid var(--accent)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', background: 'var(--bg-input)', color: 'var(--text-primary)', fontFamily: 'inherit', ...style }} placeholder={placeholder} /> @@ -131,6 +151,7 @@ function AddItemRow({ onAdd, t }: AddItemRowProps) { setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} + onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } setPrice(t) }} placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index 461d7006..ad17acd6 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -678,7 +678,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
{ const v = e.target.value; if (v === '' || /^\d*\.?\d{0,2}$/.test(v)) set('price', v) }} + onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }} + onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } set('price', t) }} placeholder="0.00" style={inputStyle} />