Add field vaidations, general improvements
This commit is contained in:
@@ -67,9 +67,9 @@ const MedPlanAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||||
{/* Left Column - Controls */}
|
{/* Left Column - Controls */}
|
||||||
<div className="lg:col-span-1 space-y-6 lg:order-1">
|
<div className="xl:col-span-1 space-y-6">
|
||||||
<DoseSchedule
|
<DoseSchedule
|
||||||
doses={doses}
|
doses={doses}
|
||||||
doseIncrement={doseIncrement}
|
doseIncrement={doseIncrement}
|
||||||
@@ -95,7 +95,7 @@ const MedPlanAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center Column - Chart */}
|
{/* Center Column - Chart */}
|
||||||
<div className="lg:col-span-2 bg-white p-5 rounded-lg shadow-sm border min-h-[600px] flex flex-col lg:order-2">
|
<div className="xl:col-span-1 bg-white p-5 rounded-lg shadow-sm border min-h-[600px] flex flex-col">
|
||||||
<div className="flex justify-center space-x-2 mb-4">
|
<div className="flex justify-center space-x-2 mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => updateUiSetting('chartView', 'damph')}
|
onClick={() => updateUiSetting('chartView', 'damph')}
|
||||||
@@ -133,7 +133,7 @@ const MedPlanAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Settings */}
|
{/* Right Column - Settings */}
|
||||||
<div className="lg:col-span-1 space-y-6 lg:order-3">
|
<div className="xl:col-span-1 space-y-6">
|
||||||
<Settings
|
<Settings
|
||||||
pkParams={pkParams}
|
pkParams={pkParams}
|
||||||
therapeuticRange={therapeuticRange}
|
therapeuticRange={therapeuticRange}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const DeviationList = ({
|
|||||||
<TimeInput
|
<TimeInput
|
||||||
value={dev.time}
|
value={dev.time}
|
||||||
onChange={newTime => onDeviationChange(index, 'time', newTime)}
|
onChange={newTime => onDeviationChange(index, 'time', newTime)}
|
||||||
|
errorMessage={t.timeRequired}
|
||||||
/>
|
/>
|
||||||
<div className="w-32">
|
<div className="w-32">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
@@ -36,6 +37,7 @@ const DeviationList = ({
|
|||||||
increment={doseIncrement}
|
increment={doseIncrement}
|
||||||
min={0}
|
min={0}
|
||||||
unit={t.mg}
|
unit={t.mg}
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => {
|
|||||||
<TimeInput
|
<TimeInput
|
||||||
value={dose.time}
|
value={dose.time}
|
||||||
onChange={newTime => onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
|
onChange={newTime => onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
|
||||||
|
errorMessage={t.timeRequired}
|
||||||
/>
|
/>
|
||||||
<div className="w-40">
|
<div className="w-40">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
@@ -19,6 +20,7 @@ const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => {
|
|||||||
increment={doseIncrement}
|
increment={doseIncrement}
|
||||||
min={0}
|
min={0}
|
||||||
unit={t.mg}
|
unit={t.mg}
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-600 text-sm flex-1">{t[dose.label] || dose.label}</span>
|
<span className="text-gray-600 text-sm flex-1">{t[dose.label] || dose.label}</span>
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ const NumericInput = ({
|
|||||||
max = Infinity,
|
max = Infinity,
|
||||||
placeholder,
|
placeholder,
|
||||||
unit,
|
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
|
// Determine decimal places based on increment
|
||||||
const getDecimalPlaces = () => {
|
const getDecimalPlaces = () => {
|
||||||
const inc = String(increment || '1');
|
const inc = String(increment || '1');
|
||||||
@@ -30,13 +35,17 @@ const NumericInput = ({
|
|||||||
const updateValue = (direction) => {
|
const updateValue = (direction) => {
|
||||||
const numIncrement = parseFloat(increment) || 1;
|
const numIncrement = parseFloat(increment) || 1;
|
||||||
let numValue = Number(value);
|
let numValue = Number(value);
|
||||||
|
|
||||||
|
// If value is empty/Auto, treat as 0
|
||||||
if (isNaN(numValue)) {
|
if (isNaN(numValue)) {
|
||||||
numValue = min !== -Infinity ? min : 0;
|
numValue = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
numValue += direction * numIncrement;
|
numValue += direction * numIncrement;
|
||||||
numValue = Math.max(min, numValue);
|
numValue = Math.max(min, numValue);
|
||||||
numValue = Math.min(max, numValue);
|
numValue = Math.min(max, numValue);
|
||||||
const finalValue = formatValue(numValue);
|
const finalValue = formatValue(numValue);
|
||||||
|
setHasError(false); // Clear error when using buttons
|
||||||
onChange(finalValue);
|
onChange(finalValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,14 +62,23 @@ const NumericInput = ({
|
|||||||
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
|
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
|
||||||
// Prevent object values
|
// Prevent object values
|
||||||
if (typeof val === 'object') return;
|
if (typeof val === 'object') return;
|
||||||
|
setHasError(false); // Clear error on input
|
||||||
onChange(val);
|
onChange(val);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (e) => {
|
const handleBlur = (e) => {
|
||||||
const val = e.target.value;
|
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))) {
|
if (val !== '' && !isNaN(Number(val))) {
|
||||||
// Format the value when user finishes editing
|
// Format the value when user finishes editing
|
||||||
|
setHasError(false);
|
||||||
onChange(formatValue(val));
|
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 (
|
return (
|
||||||
<div className="flex items-center w-full">
|
<div
|
||||||
<button
|
ref={containerRef}
|
||||||
onClick={() => updateValue(-1)}
|
className="relative"
|
||||||
className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
|
onMouseEnter={handleMouseEnter}
|
||||||
tabIndex={-1}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
-
|
<div className="relative inline-flex items-center">
|
||||||
</button>
|
<button
|
||||||
<input
|
onClick={() => updateValue(-1)}
|
||||||
type="text"
|
className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
|
||||||
value={value}
|
tabIndex={-1}
|
||||||
onChange={handleChange}
|
>
|
||||||
onKeyDown={handleKeyDown}
|
-
|
||||||
onBlur={handleBlur}
|
</button>
|
||||||
placeholder={placeholder}
|
<input
|
||||||
style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }}
|
type="text"
|
||||||
className={`p-2 border-t border-b text-sm ${getAlignmentClass()}`}
|
value={value}
|
||||||
/>
|
onChange={handleChange}
|
||||||
<button
|
onKeyDown={handleKeyDown}
|
||||||
onClick={() => updateValue(1)}
|
onBlur={handleInputBlurWrapper}
|
||||||
className="px-2 py-1 border rounded-r-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
|
onFocus={handleFocus}
|
||||||
tabIndex={-1}
|
placeholder={placeholder}
|
||||||
>
|
style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }}
|
||||||
+
|
className={`p-2 border-t border-b text-sm ${getAlignmentClass()} ${hasError ? 'bg-red-50' : ''}`}
|
||||||
</button>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => 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}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
{allowEmpty && (
|
||||||
|
<button
|
||||||
|
onClick={handleClear}
|
||||||
|
className="px-2 py-1 border rounded-r-md bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800"
|
||||||
|
tabIndex={-1}
|
||||||
|
title="Clear (set to Auto)"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{hasError && (
|
||||||
|
<div className="absolute inset-0 border-2 border-red-500 rounded-md pointer-events-none" style={{ zIndex: 10 }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
|
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
|
||||||
|
{hasError && showErrorTooltip && (
|
||||||
|
<div
|
||||||
|
className={`absolute z-50 bg-white border-2 border-red-500 rounded-md shadow-lg p-2 flex items-start gap-2 ${
|
||||||
|
tooltipPosition.top ? 'bottom-full mb-1' : 'top-full mt-1'
|
||||||
|
} ${
|
||||||
|
tooltipPosition.left ? 'right-0' : 'left-0'
|
||||||
|
}`}
|
||||||
|
style={{ minWidth: '200px', maxWidth: '400px' }}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
<svg className="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#fa5252"/>
|
||||||
|
<path d="M17 15.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-red-700 whitespace-nowrap">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const Settings = ({
|
|||||||
min={2}
|
min={2}
|
||||||
max={7}
|
max={7}
|
||||||
unit={t.days}
|
unit={t.days}
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,19 +52,20 @@ const Settings = ({
|
|||||||
min={1}
|
min={1}
|
||||||
max={parseInt(simulationDays, 10) || 1}
|
max={parseInt(simulationDays, 10) || 1}
|
||||||
unit={t.days}
|
unit={t.days}
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="block font-medium text-gray-600 pt-2">{t.yAxisRange}</label>
|
<label className="block font-medium text-gray-600 pt-2">{t.yAxisRange}</label>
|
||||||
<div className="flex items-center space-x-3 mt-1">
|
<div className="flex items-center mt-1">
|
||||||
<div className="w-32">
|
<div className="w-30">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
value={yAxisMin}
|
value={yAxisMin}
|
||||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||||
increment={'5'}
|
increment={'5'}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.auto}
|
placeholder={t.auto}
|
||||||
unit="ng/ml"
|
allowEmpty={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-500 px-2">-</span>
|
<span className="text-gray-500 px-2">-</span>
|
||||||
@@ -75,20 +77,21 @@ const Settings = ({
|
|||||||
min={0}
|
min={0}
|
||||||
placeholder={t.auto}
|
placeholder={t.auto}
|
||||||
unit="ng/ml"
|
unit="ng/ml"
|
||||||
|
allowEmpty={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="block font-medium text-gray-600">{t.therapeuticRange}</label>
|
<label className="block font-medium text-gray-600">{t.therapeuticRange}</label>
|
||||||
<div className="flex items-center space-x-3 mt-1">
|
<div className="flex items-center mt-1">
|
||||||
<div className="w-32">
|
<div className="w-30">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
value={therapeuticRange.min}
|
value={therapeuticRange.min}
|
||||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||||
increment={'0.5'}
|
increment={'0.5'}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.min}
|
placeholder={t.min}
|
||||||
unit="ng/ml"
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-500 px-2">-</span>
|
<span className="text-gray-500 px-2">-</span>
|
||||||
@@ -100,6 +103,7 @@ const Settings = ({
|
|||||||
min={0}
|
min={0}
|
||||||
placeholder={t.max}
|
placeholder={t.max}
|
||||||
unit="ng/ml"
|
unit="ng/ml"
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,6 +117,7 @@ const Settings = ({
|
|||||||
increment={'0.5'}
|
increment={'0.5'}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
unit="h"
|
unit="h"
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,6 +130,7 @@ const Settings = ({
|
|||||||
increment={'0.1'}
|
increment={'0.1'}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
unit="h"
|
unit="h"
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-40">
|
<div className="w-40">
|
||||||
@@ -135,6 +141,7 @@ const Settings = ({
|
|||||||
increment={'0.1'}
|
increment={'0.1'}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
unit={t.faster}
|
unit={t.faster}
|
||||||
|
errorMessage={t.fieldRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const TimeInput = ({ value, onChange }) => {
|
const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
|
||||||
const [displayValue, setDisplayValue] = React.useState(value);
|
const [displayValue, setDisplayValue] = React.useState(value);
|
||||||
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
|
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 [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number);
|
||||||
const pickerRef = React.useRef(null);
|
const pickerRef = React.useRef(null);
|
||||||
|
const containerRef = React.useRef(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
@@ -16,19 +19,32 @@ const TimeInput = ({ value, onChange }) => {
|
|||||||
if (pickerRef.current && !pickerRef.current.contains(event.target)) {
|
if (pickerRef.current && !pickerRef.current.contains(event.target)) {
|
||||||
setIsPickerOpen(false);
|
setIsPickerOpen(false);
|
||||||
}
|
}
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||||
|
setShowErrorTooltip(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isPickerOpen) {
|
if (isPickerOpen || showErrorTooltip) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [isPickerOpen]);
|
}, [isPickerOpen, showErrorTooltip]);
|
||||||
|
|
||||||
const handleBlur = (e) => {
|
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';
|
let hours = '00', minutes = '00';
|
||||||
if (input.length <= 2) {
|
if (input.length <= 2) {
|
||||||
hours = input.padStart(2, '0');
|
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');
|
hours = Math.min(23, parseInt(hours, 10) || 0).toString().padStart(2, '0');
|
||||||
minutes = Math.min(59, parseInt(minutes, 10) || 0).toString().padStart(2, '0');
|
minutes = Math.min(59, parseInt(minutes, 10) || 0).toString().padStart(2, '0');
|
||||||
const formattedTime = `${hours}:${minutes}`;
|
const formattedTime = `${hours}:${minutes}`;
|
||||||
|
setHasError(false);
|
||||||
|
setShowErrorTooltip(false);
|
||||||
setDisplayValue(formattedTime);
|
setDisplayValue(formattedTime);
|
||||||
onChange(formattedTime);
|
onChange(formattedTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
|
setHasError(false); // Clear error on input
|
||||||
|
setShowErrorTooltip(false);
|
||||||
setDisplayValue(e.target.value);
|
setDisplayValue(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,19 +80,42 @@ const TimeInput = ({ value, onChange }) => {
|
|||||||
newMinutes = val;
|
newMinutes = val;
|
||||||
}
|
}
|
||||||
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
|
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
|
||||||
|
setHasError(false); // Clear error when using picker
|
||||||
|
setShowErrorTooltip(false);
|
||||||
onChange(formattedTime);
|
onChange(formattedTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (hasError) setShowErrorTooltip(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (hasError) setShowErrorTooltip(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
setShowErrorTooltip(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div
|
||||||
<input
|
ref={containerRef}
|
||||||
type="text"
|
className="relative"
|
||||||
value={displayValue}
|
onMouseEnter={handleMouseEnter}
|
||||||
onChange={handleChange}
|
onMouseLeave={handleMouseLeave}
|
||||||
onBlur={handleBlur}
|
>
|
||||||
placeholder="HH:MM"
|
<div className="flex items-center">
|
||||||
className="p-2 border rounded-md w-24 text-sm text-center"
|
<div className={`${hasError ? 'ring-2 ring-red-500 rounded-md' : ''}`}>
|
||||||
/>
|
<input
|
||||||
|
type="text"
|
||||||
|
value={displayValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
placeholder="HH:MM"
|
||||||
|
className={`p-2 border rounded-md w-24 text-sm text-center ${hasError ? 'bg-red-50 border-transparent' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsPickerOpen(!isPickerOpen)}
|
onClick={() => setIsPickerOpen(!isPickerOpen)}
|
||||||
className="ml-2 p-2 text-gray-500 hover:text-gray-700"
|
className="ml-2 p-2 text-gray-500 hover:text-gray-700"
|
||||||
@@ -82,7 +125,7 @@ const TimeInput = ({ value, onChange }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{isPickerOpen && (
|
{isPickerOpen && (
|
||||||
<div ref={pickerRef} className="absolute top-full mt-2 z-10 bg-white p-4 rounded-lg shadow-xl border w-64">
|
<div ref={pickerRef} className="absolute top-full mt-2 z-[60] bg-white p-4 rounded-lg shadow-xl border w-64">
|
||||||
<div className="text-center text-lg font-bold mb-3">{value}</div>
|
<div className="text-center text-lg font-bold mb-3">{value}</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2"><span className="font-semibold">Hour:</span></div>
|
<div className="mb-2"><span className="font-semibold">Hour:</span></div>
|
||||||
@@ -120,6 +163,18 @@ const TimeInput = ({ value, onChange }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{hasError && showErrorTooltip && (
|
||||||
|
<div className="absolute left-0 top-full mt-1 z-50 bg-white border-2 border-red-500 rounded-md shadow-lg p-2 flex items-start gap-2" style={{ maxWidth: '28rem' }}>
|
||||||
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
<svg className="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#fa5252"/>
|
||||||
|
<path d="M17 15.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-red-700 break-words">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const getDefaultState = () => ({
|
|||||||
{ time: '06:30', dose: '25', label: 'morning' },
|
{ time: '06:30', dose: '25', label: 'morning' },
|
||||||
{ time: '12:30', dose: '10', label: 'midday' },
|
{ time: '12:30', dose: '10', label: 'midday' },
|
||||||
{ time: '17:00', dose: '10', label: 'afternoon' },
|
{ 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' },
|
{ time: '01:00', dose: '0', label: 'night' },
|
||||||
],
|
],
|
||||||
steadyStateConfig: { daysOnMedication: '7' },
|
steadyStateConfig: { daysOnMedication: '7' },
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ export const de = {
|
|||||||
|
|
||||||
// Footer disclaimer
|
// Footer disclaimer
|
||||||
importantNote: "Wichtiger Hinweis",
|
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;
|
export default de;
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ export const en = {
|
|||||||
|
|
||||||
// Footer disclaimer
|
// Footer disclaimer
|
||||||
importantNote: "Important Notice",
|
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;
|
export default en;
|
||||||
|
|||||||
Reference in New Issue
Block a user