+
onDeviationChange(index, 'time', newTime)}
+ errorMessage={t.timeRequired}
/>
{
onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
+ errorMessage={t.timeRequired}
/>
{
increment={doseIncrement}
min={0}
unit={t.mg}
+ errorMessage={t.fieldRequired}
/>
{t[dose.label] || dose.label}
diff --git a/src/components/NumericInput.js b/src/components/NumericInput.js
index 2cc98dd..c3d3a4b 100644
--- a/src/components/NumericInput.js
+++ b/src/components/NumericInput.js
@@ -8,8 +8,13 @@ const NumericInput = ({
max = Infinity,
placeholder,
unit,
- align = 'right' // 'left', 'center', 'right'
+ align = 'right', // 'left', 'center', 'right'
+ allowEmpty = false, // Allow empty value (e.g., for "Auto" mode)
+ errorMessage = 'This field is required' // Error message for mandatory empty fields
}) => {
+ const [hasError, setHasError] = React.useState(false);
+ const [showErrorTooltip, setShowErrorTooltip] = React.useState(false);
+ const containerRef = React.useRef(null);
// Determine decimal places based on increment
const getDecimalPlaces = () => {
const inc = String(increment || '1');
@@ -30,13 +35,17 @@ const NumericInput = ({
const updateValue = (direction) => {
const numIncrement = parseFloat(increment) || 1;
let numValue = Number(value);
+
+ // If value is empty/Auto, treat as 0
if (isNaN(numValue)) {
- numValue = min !== -Infinity ? min : 0;
+ numValue = 0;
}
+
numValue += direction * numIncrement;
numValue = Math.max(min, numValue);
numValue = Math.min(max, numValue);
const finalValue = formatValue(numValue);
+ setHasError(false); // Clear error when using buttons
onChange(finalValue);
};
@@ -53,14 +62,23 @@ const NumericInput = ({
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
// Prevent object values
if (typeof val === 'object') return;
+ setHasError(false); // Clear error on input
onChange(val);
}
};
const handleBlur = (e) => {
const val = e.target.value;
+
+ // Check if field is empty and not allowed to be empty
+ if (val === '' && !allowEmpty) {
+ setHasError(true);
+ return;
+ }
+
if (val !== '' && !isNaN(Number(val))) {
// Format the value when user finishes editing
+ setHasError(false);
onChange(formatValue(val));
}
};
@@ -75,33 +93,136 @@ const NumericInput = ({
}
};
+ const handleClear = () => {
+ setHasError(false);
+ setShowErrorTooltip(false);
+ onChange('');
+ };
+
+ const handleFocus = () => {
+ if (hasError) setShowErrorTooltip(true);
+ };
+
+ const handleInputBlurWrapper = (e) => {
+ handleBlur(e);
+ // Small delay to allow error state to be set before hiding tooltip
+ setTimeout(() => {
+ setShowErrorTooltip(false);
+ }, 100);
+ };
+
+ const handleMouseEnter = () => {
+ if (hasError) setShowErrorTooltip(true);
+ };
+
+ const handleMouseLeave = () => {
+ setShowErrorTooltip(false);
+ };
+
+ React.useEffect(() => {
+ // Close tooltip when clicking outside
+ const handleClickOutside = (event) => {
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
+ setShowErrorTooltip(false);
+ }
+ };
+
+ if (showErrorTooltip) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [showErrorTooltip]);
+
+ // Calculate tooltip position to prevent overflow
+ const [tooltipPosition, setTooltipPosition] = React.useState({ top: true, left: true });
+
+ React.useEffect(() => {
+ if (showErrorTooltip && containerRef.current) {
+ const rect = containerRef.current.getBoundingClientRect();
+ const tooltipWidth = 300; // Approximate width
+ const tooltipHeight = 60; // Approximate height
+
+ const spaceRight = window.innerWidth - rect.right;
+ const spaceBottom = window.innerHeight - rect.bottom;
+
+ setTooltipPosition({
+ top: spaceBottom < tooltipHeight + 10,
+ left: spaceRight < tooltipWidth
+ });
+ }
+ }, [showErrorTooltip]);
+
return (
-
-
updateValue(-1)}
- className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
- tabIndex={-1}
- >
- -
-
-
-
updateValue(1)}
- className="px-2 py-1 border rounded-r-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
- tabIndex={-1}
- >
- +
-
+
+
+
updateValue(-1)}
+ className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
+ tabIndex={-1}
+ >
+ -
+
+
+
updateValue(1)}
+ className={`px-2 py-1 border bg-gray-100 hover:bg-gray-200 text-lg font-bold ${!allowEmpty ? 'rounded-r-md' : ''}`}
+ tabIndex={-1}
+ >
+ +
+
+ {allowEmpty && (
+
+
+
+
+
+ )}
+ {hasError && (
+
+ )}
+
{unit &&
{unit} }
+ {hasError && showErrorTooltip && (
+
+ )}
);
};
diff --git a/src/components/Settings.js b/src/components/Settings.js
index b89adcc..c0a82d7 100644
--- a/src/components/Settings.js
+++ b/src/components/Settings.js
@@ -39,6 +39,7 @@ const Settings = ({
min={2}
max={7}
unit={t.days}
+ errorMessage={t.fieldRequired}
/>
@@ -51,19 +52,20 @@ const Settings = ({
min={1}
max={parseInt(simulationDays, 10) || 1}
unit={t.days}
+ errorMessage={t.fieldRequired}
/>
{t.yAxisRange}
-
-
+
+
onUpdateUiSetting('yAxisMin', val)}
increment={'5'}
min={0}
placeholder={t.auto}
- unit="ng/ml"
+ allowEmpty={true}
/>
-
@@ -75,20 +77,21 @@ const Settings = ({
min={0}
placeholder={t.auto}
unit="ng/ml"
+ allowEmpty={true}
/>
{t.therapeuticRange}
-
-
+
+
onUpdateTherapeuticRange('min', val)}
increment={'0.5'}
min={0}
placeholder={t.min}
- unit="ng/ml"
+ errorMessage={t.fieldRequired}
/>
-
@@ -100,6 +103,7 @@ const Settings = ({
min={0}
placeholder={t.max}
unit="ng/ml"
+ errorMessage={t.fieldRequired}
/>
@@ -113,6 +117,7 @@ const Settings = ({
increment={'0.5'}
min={0.1}
unit="h"
+ errorMessage={t.fieldRequired}
/>
@@ -125,6 +130,7 @@ const Settings = ({
increment={'0.1'}
min={0.1}
unit="h"
+ errorMessage={t.fieldRequired}
/>
@@ -135,6 +141,7 @@ const Settings = ({
increment={'0.1'}
min={0.1}
unit={t.faster}
+ errorMessage={t.fieldRequired}
/>
diff --git a/src/components/TimeInput.js b/src/components/TimeInput.js
index b438b8d..2aa3852 100644
--- a/src/components/TimeInput.js
+++ b/src/components/TimeInput.js
@@ -1,10 +1,13 @@
import React from 'react';
-const TimeInput = ({ value, onChange }) => {
+const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
const [displayValue, setDisplayValue] = React.useState(value);
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
+ const [hasError, setHasError] = React.useState(false);
+ const [showErrorTooltip, setShowErrorTooltip] = React.useState(false);
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number);
const pickerRef = React.useRef(null);
+ const containerRef = React.useRef(null);
React.useEffect(() => {
setDisplayValue(value);
@@ -16,19 +19,32 @@ const TimeInput = ({ value, onChange }) => {
if (pickerRef.current && !pickerRef.current.contains(event.target)) {
setIsPickerOpen(false);
}
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
+ setShowErrorTooltip(false);
+ }
};
- if (isPickerOpen) {
+ if (isPickerOpen || showErrorTooltip) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
- }, [isPickerOpen]);
+ }, [isPickerOpen, showErrorTooltip]);
const handleBlur = (e) => {
- let input = e.target.value.replace(/[^0-9]/g, '');
+ const inputValue = e.target.value.trim();
+
+ // Check if field is empty
+ if (inputValue === '') {
+ setHasError(true);
+ // Small delay before hiding tooltip
+ setTimeout(() => setShowErrorTooltip(false), 100);
+ return;
+ }
+
+ let input = inputValue.replace(/[^0-9]/g, '');
let hours = '00', minutes = '00';
if (input.length <= 2) {
hours = input.padStart(2, '0');
@@ -44,11 +60,15 @@ const TimeInput = ({ value, onChange }) => {
hours = Math.min(23, parseInt(hours, 10) || 0).toString().padStart(2, '0');
minutes = Math.min(59, parseInt(minutes, 10) || 0).toString().padStart(2, '0');
const formattedTime = `${hours}:${minutes}`;
+ setHasError(false);
+ setShowErrorTooltip(false);
setDisplayValue(formattedTime);
onChange(formattedTime);
};
const handleChange = (e) => {
+ setHasError(false); // Clear error on input
+ setShowErrorTooltip(false);
setDisplayValue(e.target.value);
};
@@ -60,19 +80,42 @@ const TimeInput = ({ value, onChange }) => {
newMinutes = val;
}
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
+ setHasError(false); // Clear error when using picker
+ setShowErrorTooltip(false);
onChange(formattedTime);
};
+ const handleFocus = () => {
+ if (hasError) setShowErrorTooltip(true);
+ };
+
+ const handleMouseEnter = () => {
+ if (hasError) setShowErrorTooltip(true);
+ };
+
+ const handleMouseLeave = () => {
+ setShowErrorTooltip(false);
+ };
+
return (
-
-
+
+
+
+
+
setIsPickerOpen(!isPickerOpen)}
className="ml-2 p-2 text-gray-500 hover:text-gray-700"
@@ -82,7 +125,7 @@ const TimeInput = ({ value, onChange }) => {
{isPickerOpen && (
-
+
{value}
Hour:
@@ -120,6 +163,18 @@ const TimeInput = ({ value, onChange }) => {
)}
+ {hasError && showErrorTooltip && (
+
+ )}
+
);
};
diff --git a/src/constants/defaults.js b/src/constants/defaults.js
index 2facd99..5692b18 100644
--- a/src/constants/defaults.js
+++ b/src/constants/defaults.js
@@ -12,7 +12,7 @@ export const getDefaultState = () => ({
{ time: '06:30', dose: '25', label: 'morning' },
{ time: '12:30', dose: '10', label: 'midday' },
{ time: '17:00', dose: '10', label: 'afternoon' },
- { time: '21:00', dose: '10', label: 'evening' },
+ { time: '22:00', dose: '10', label: 'evening' },
{ time: '01:00', dose: '0', label: 'night' },
],
steadyStateConfig: { daysOnMedication: '7' },
diff --git a/src/locales/de.js b/src/locales/de.js
index cb8ab89..8b18960 100644
--- a/src/locales/de.js
+++ b/src/locales/de.js
@@ -71,7 +71,11 @@ export const de = {
// Footer disclaimer
importantNote: "Wichtiger Hinweis",
- disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst."
+ disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.",
+
+ // Field validation
+ fieldRequired: "Dieses Feld ist erforderlich",
+ timeRequired: "Zeitangabe ist erforderlich"
};
export default de;
diff --git a/src/locales/en.js b/src/locales/en.js
index 339a74b..952f317 100644
--- a/src/locales/en.js
+++ b/src/locales/en.js
@@ -71,7 +71,11 @@ export const en = {
// Footer disclaimer
importantNote: "Important Notice",
- disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication."
+ disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication.",
+
+ // Field validation
+ fieldRequired: "This field is required",
+ timeRequired: "Time is required"
};
export default en;