diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 0556e39..98c39de 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -12,7 +12,7 @@ import Input from '@mui/joy/Input' import Option from '@mui/joy/Option' import Select from '@mui/joy/Select' import Typography from '@mui/joy/Typography' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useRef } from 'react' import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors' import { TIME_UNITS } from '../utils/DurationUtils' @@ -73,6 +73,9 @@ const NotificationTemplate = ({ [], ) + const notificationsRef = useRef(notifications) + const [draftValues, setDraftValues] = useState({}) + const [error, setError] = useState(null) const [showSaveDefault, setShowSaveDefault] = useState(false) // Create a map of notification indices for timeline display @@ -114,7 +117,6 @@ const NotificationTemplate = ({ // Sort notifications and update the index mapping useEffect(() => { updateNotificationIndices() - setError(null) }, [updateNotificationIndices]) // Notify parent component of changes including the template name @@ -125,8 +127,8 @@ const NotificationTemplate = ({ }, [notifications, onChange]) // Validates if a notification configuration already exists - const isDuplicate = (notification, currentIdx = -1) => { - return notifications.some((n, idx) => { + const isDuplicate = (notification, currentIdx = -1, list = notifications) => { + return list.some((n, idx) => { if (idx === currentIdx) return false return ( @@ -136,6 +138,26 @@ const NotificationTemplate = ({ }) } + const getSmartSuggestion = type => { + let suggestions = [] + if (type === 'reminder' || type === 'before') { + suggestions = [ + { value: -1, unit: 'd' }, + { value: -3, unit: 'h' }, + { value: -30, unit: 'm' }, + ] + } else if (type === 'followup' || type === 'after') { + suggestions = [ + { value: 1, unit: 'd' }, + { value: 3, unit: 'd' }, + { value: 7, unit: 'd' }, + ] + } + return suggestions.find( + suggestion => !isDuplicate(suggestion, -1, notificationsRef.current), + ) + } + const handleChange = (idx, field, value) => { const currentNotification = notifications[idx] const uiRep = getUIRepresentation(currentNotification) @@ -149,6 +171,15 @@ const NotificationTemplate = ({ // Reset display value when switching to "On Due" if (value === 'ondue') { updatedUIRep.displayValue = 0 + } else if (Number(currentNotification.value) === 0) { + const suggestion = getSmartSuggestion(value) + if (suggestion) { + updatedUIRep.displayValue = Math.abs(suggestion.value) + updatedUIRep.unit = suggestion.unit + } else { + updatedUIRep.displayValue = 1 + updatedUIRep.unit = 'h' + } } } else if (field === 'displayValue') { updatedUIRep.displayValue = Math.max(0, Number(value)) @@ -168,71 +199,41 @@ const NotificationTemplate = ({ unit: updatedUIRep.unit, } - // Check if another notification is already "On Due" (value = 0) - if (newInternalValue === 0) { - const existingOnDue = notifications.findIndex( - (n, i) => i !== idx && Number(n.value) === 0, - ) + const updated = notifications.map((n, i) => + i === idx ? updatedNotification : n, + ) + setNotifications(updated) + notificationsRef.current = updated + setError(null) + } - if (existingOnDue !== -1) { - setError( - 'Only one notification can be set to "On Due". Please choose a different timing.', - ) - return - } - } + const handleBlur = idx => { + const currentList = notificationsRef.current + const currentNotification = currentList[idx] - if (isDuplicate(updatedNotification, idx)) { + if (!currentNotification) return + + if (isDuplicate(currentNotification, idx, currentList)) { setError( 'This notification setting already exists. Please use a different timing.', ) return } - - const updated = notifications.map((n, i) => - i === idx ? updatedNotification : n, - ) - setNotifications(updated) - setError(null) } const addSmartNotification = type => { if (notifications.length >= maxNotifications) return setShowSaveDefault(true) let newNotification - let suggestions = [] - - switch (type) { - case 'reminder': - // Suggest common reminder times that don't exist - suggestions = [ - { value: -1, unit: 'd' }, // 1 day before - { value: -3, unit: 'h' }, // 3 hours before - { value: -30, unit: 'm' }, // 3 days before - ] - break - - case 'due': - if (notifications.some(n => Number(n.value) === 0)) { - setError('Only one "Due Alert" notification is allowed.') - return - } - newNotification = { value: 0, unit: 'm' } - break - - case 'followup': - suggestions = [ - { value: 1, unit: 'd' }, // 1 day after - { value: 3, unit: 'd' }, // 3 days after - { value: 7, unit: 'd' }, // 1 week after - ] - break - } - - // For reminder/followup, find first non-duplicate suggestion - if (suggestions.length > 0) { - newNotification = suggestions.find(suggestion => !isDuplicate(suggestion)) + if (type === 'due') { + if (notificationsRef.current.some(n => Number(n.value) === 0)) { + setError('Only one "Due Alert" notification is allowed.') + return + } + newNotification = { value: 0, unit: 'm' } + } else { + newNotification = getSmartSuggestion(type) if (!newNotification) { setError(`All common ${type} times are already configured.`) return @@ -243,15 +244,25 @@ const NotificationTemplate = ({ const updatedNotifications = [...notifications, newNotification] setNotifications(updatedNotifications) + notificationsRef.current = updatedNotifications setError(null) } const removeNotification = idx => { const updated = notifications.filter((_, i) => i !== idx) setNotifications(updated) + notificationsRef.current = updated + + setDraftValues(prev => { + const next = { ...prev } + delete next[idx] + return next + }) + onChange && onChange(updated) setShowSaveDefault(true) } + const renderTimeline = () => { // Convert notifications to minutes for proper chronological sorting const convertToMinutes = (value, unit) => { @@ -459,6 +470,11 @@ const NotificationTemplate = ({ const badgeNumber = notificationIndexMap[idx] const uiRep = getUIRepresentation(n) + // Check if an "On Due" notification exists anywhere else in the list + const hasOnDueElsewhere = notificationsRef.current.some( + (notif, i) => i !== idx && Number(notif.value) === 0, + ) + const getNotificationColors = value => { if (Number(value) < 0) { return { @@ -487,7 +503,7 @@ const NotificationTemplate = ({ const colors = getNotificationColors(n.value) return ( - <> + handleChange(idx, 'timing', value)} + onBlur={() => handleBlur(idx)} sx={{ minWidth: 80 }} size={'sm'} > {timingOptions.map(opt => ( - ))} @@ -560,11 +579,62 @@ const NotificationTemplate = ({ - handleChange(idx, 'displayValue', e.target.value) + value={ + draftValues[idx] !== undefined + ? draftValues[idx] + : uiRep.displayValue } + disabled={uiRep.timing === 'ondue'} + onChange={e => { + const val = e.target.value + if (val.includes('-')) return + + // Prevent setting to '0' if an 'On Due' already exists elsewhere + if (val === '0' || val === '') { + if (hasOnDueElsewhere) return + } + + setDraftValues(prev => ({ ...prev, [idx]: val })) + }} + onKeyDown={e => { + if (['-', 'e', '+', '.'].includes(e.key)) { + e.preventDefault() + return + } + + // Physically block typing '0' if 'On Due' exists + if ( + e.key === '0' || + e.key === 'Backspace' || + e.key === 'Delete' + ) { + const currentVal = + draftValues[idx] !== undefined + ? draftValues[idx] + : uiRep.displayValue.toString() + + // If typing 0 into an empty input, or deleting the last character + const willBeZero = + (e.key === '0' && currentVal === '') || + ((e.key === 'Backspace' || e.key === 'Delete') && + currentVal.length <= 1) + + if (willBeZero && hasOnDueElsewhere) { + e.preventDefault() + } + } + }} + onBlur={e => { + let val = e.target.value + if (val === '' || Number(val) < 0) val = 0 + handleChange(idx, 'displayValue', val) + setDraftValues(prev => { + const next = { ...prev } + delete next[idx] + return next + }) + handleBlur(idx) + }} sx={{ width: 60, opacity: uiRep.timing === 'ondue' ? 0.6 : 1, @@ -576,6 +646,7 @@ const NotificationTemplate = ({ value={n.unit} disabled={uiRep.timing === 'ondue'} onChange={(_, value) => handleChange(idx, 'unit', value)} + onBlur={() => handleBlur(idx)} sx={{ minWidth: 70, opacity: uiRep.timing === 'ondue' ? 0.6 : 1, @@ -605,7 +676,7 @@ const NotificationTemplate = ({ - + ) })}