774 lines
25 KiB
JavaScript
774 lines
25 KiB
JavaScript
import { Save } from '@mui/icons-material'
|
|
import AddIcon from '@mui/icons-material/Add'
|
|
import DeleteIcon from '@mui/icons-material/Delete'
|
|
import InfoIcon from '@mui/icons-material/Info'
|
|
import NotificationsIcon from '@mui/icons-material/Notifications'
|
|
import Alert from '@mui/joy/Alert'
|
|
import Badge from '@mui/joy/Badge'
|
|
import Box from '@mui/joy/Box'
|
|
import Button from '@mui/joy/Button'
|
|
import IconButton from '@mui/joy/IconButton'
|
|
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, useRef, useState } from 'react'
|
|
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
|
import { TIME_UNITS } from '../utils/DurationUtils'
|
|
|
|
const timeUnits = TIME_UNITS
|
|
|
|
const timingOptions = [
|
|
{ label: 'Before', value: 'before' },
|
|
{ label: 'Due', value: 'ondue' },
|
|
{ label: 'After', value: 'after' },
|
|
]
|
|
|
|
function getRelativeLabel(notification) {
|
|
const { value, unit } = notification
|
|
const numericValue = Number(value)
|
|
if (numericValue === 0) {
|
|
return 'On due date'
|
|
}
|
|
const unitName = unit === 'm' ? 'minutes' : unit === 'h' ? 'hours' : 'days'
|
|
const absValue = Math.abs(numericValue)
|
|
return `${absValue} ${unitName} ${numericValue < 0 ? 'before' : 'after'} due`
|
|
}
|
|
|
|
// Helper functions to convert between internal value and UI representation
|
|
function getUIRepresentation(notification) {
|
|
const numericValue = Number(notification.value)
|
|
if (numericValue === 0) {
|
|
return { timing: 'ondue', displayValue: 0, unit: notification.unit }
|
|
} else if (numericValue < 0) {
|
|
return {
|
|
timing: 'before',
|
|
displayValue: Math.abs(numericValue),
|
|
unit: notification.unit,
|
|
}
|
|
} else {
|
|
return {
|
|
timing: 'after',
|
|
displayValue: numericValue,
|
|
unit: notification.unit,
|
|
}
|
|
}
|
|
}
|
|
|
|
function getInternalValue(timing, displayValue) {
|
|
if (timing === 'ondue') return 0
|
|
if (timing === 'before') return -Math.abs(displayValue)
|
|
return Math.abs(displayValue) // 'after'
|
|
}
|
|
|
|
const NotificationTemplate = ({
|
|
maxNotifications = 5,
|
|
onChange,
|
|
value,
|
|
showTimeline = true,
|
|
}) => {
|
|
const [notifications, setNotifications] = useState(
|
|
value?.templates ||
|
|
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
|
[],
|
|
)
|
|
|
|
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
|
|
const [notificationIndexMap, setNotificationIndexMap] = useState({})
|
|
|
|
const updateNotificationIndices = useCallback(() => {
|
|
// Convert notifications to minutes for proper chronological sorting
|
|
const convertToMinutes = (value, unit) => {
|
|
const numericValue = Number(value)
|
|
if (numericValue === 0) return 0
|
|
let minutes = Math.abs(numericValue)
|
|
if (unit === 'h') minutes *= 60
|
|
if (unit === 'd') minutes *= 24 * 60
|
|
return numericValue < 0 ? -minutes : minutes
|
|
}
|
|
|
|
// Sort notifications for consistent ordering by actual time duration
|
|
const sorted = [...notifications].sort((a, b) => {
|
|
const aMinutes = convertToMinutes(a.value, a.unit)
|
|
const bMinutes = convertToMinutes(b.value, b.unit)
|
|
return aMinutes - bMinutes
|
|
})
|
|
|
|
const indexMap = {}
|
|
// Map original array indices to their chronological position numbers
|
|
notifications.forEach((originalNotification, originalIdx) => {
|
|
const chronologicalPosition = sorted.findIndex(
|
|
sortedNotification =>
|
|
Number(sortedNotification.value) ===
|
|
Number(originalNotification.value) &&
|
|
sortedNotification.unit === originalNotification.unit,
|
|
)
|
|
indexMap[originalIdx] = chronologicalPosition + 1
|
|
})
|
|
|
|
setNotificationIndexMap(indexMap)
|
|
}, [notifications])
|
|
|
|
// Sort notifications and update the index mapping
|
|
useEffect(() => {
|
|
updateNotificationIndices()
|
|
}, [updateNotificationIndices])
|
|
|
|
// Notify parent component of changes including the template name
|
|
useEffect(() => {
|
|
if (onChange) {
|
|
onChange({ notifications })
|
|
}
|
|
}, [notifications, onChange])
|
|
|
|
// Validates if a notification configuration already exists
|
|
const isDuplicate = (notification, currentIdx = -1, list = notifications) => {
|
|
return list.some((n, idx) => {
|
|
if (idx === currentIdx) return false
|
|
|
|
return (
|
|
Number(n.value) === Number(notification.value) &&
|
|
n.unit === notification.unit
|
|
)
|
|
})
|
|
}
|
|
|
|
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)
|
|
|
|
let updatedUIRep = { ...uiRep }
|
|
let updatedNotification = { ...currentNotification }
|
|
|
|
// Update the UI representation based on the field being changed
|
|
if (field === 'timing') {
|
|
updatedUIRep.timing = value
|
|
// 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))
|
|
} else if (field === 'unit') {
|
|
updatedUIRep.unit = value
|
|
updatedNotification.unit = value
|
|
}
|
|
|
|
// Convert back to internal representation
|
|
const newInternalValue = getInternalValue(
|
|
updatedUIRep.timing,
|
|
updatedUIRep.displayValue,
|
|
)
|
|
updatedNotification = {
|
|
...updatedNotification,
|
|
value: newInternalValue,
|
|
unit: updatedUIRep.unit,
|
|
}
|
|
|
|
const updated = notifications.map((n, i) =>
|
|
i === idx ? updatedNotification : n,
|
|
)
|
|
setNotifications(updated)
|
|
notificationsRef.current = updated
|
|
setError(null)
|
|
}
|
|
|
|
const handleBlur = idx => {
|
|
const currentList = notificationsRef.current
|
|
const currentNotification = currentList[idx]
|
|
|
|
if (!currentNotification) return
|
|
|
|
if (isDuplicate(currentNotification, idx, currentList)) {
|
|
setError(
|
|
'This notification setting already exists. Please use a different timing.',
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
const addSmartNotification = type => {
|
|
if (notifications.length >= maxNotifications) return
|
|
setShowSaveDefault(true)
|
|
let newNotification
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Add the new notification to the end (don't sort, keep form order)
|
|
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) => {
|
|
const numericValue = Number(value)
|
|
if (numericValue === 0) return 0
|
|
let minutes = Math.abs(numericValue)
|
|
if (unit === 'h') minutes *= 60
|
|
if (unit === 'd') minutes *= 24 * 60
|
|
return numericValue < 0 ? -minutes : minutes
|
|
}
|
|
|
|
// Sort notifications chronologically by actual time (in minutes)
|
|
const sorted = [...notifications].sort((a, b) => {
|
|
const aMinutes = convertToMinutes(a.value, a.unit)
|
|
const bMinutes = convertToMinutes(b.value, b.unit)
|
|
return aMinutes - bMinutes
|
|
})
|
|
|
|
// Get min and max notification times in minutes for dynamic scaling
|
|
const minutesValues = sorted.map(n => convertToMinutes(n.value, n.unit))
|
|
const minBefore = Math.min(0, ...minutesValues) // Default to 0 if no "before" notifications
|
|
const maxAfter = Math.max(0, ...minutesValues) // Default to 0 if no "after" notifications
|
|
|
|
const getPositionPercent = (value, unit) => {
|
|
const minutes = convertToMinutes(value, unit)
|
|
|
|
// Due date is always at center (50%)
|
|
if (minutes === 0) return 50
|
|
|
|
// For notifications before due date (negative values)
|
|
if (minutes < 0) {
|
|
if (minBefore === 0) return 30 // Default position if no before notifications
|
|
// Scale between 10% (furthest left) and 45% (closest to due)
|
|
return 45 - (Math.abs(minutes) / Math.abs(minBefore)) * 35
|
|
}
|
|
|
|
// For notifications after due date (positive values)
|
|
if (maxAfter === 0) return 70 // Default position if no after notifications
|
|
// Scale between 55% (closest to due) and 90% (furthest right)
|
|
return 55 + (minutes / maxAfter) * 35
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ mt: 3, mb: 2 }}>
|
|
<Typography level={'body-md'} sx={{ mb: 1 }}>
|
|
Notification Timeline
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'relative',
|
|
height: 90,
|
|
bgcolor: 'background.surface',
|
|
borderRadius: 'md',
|
|
border: '1px solid',
|
|
borderColor: 'neutral.outlinedBorder',
|
|
p: 2,
|
|
transition: 'height 0.3s ease',
|
|
// '&:hover': {
|
|
// height: 130,
|
|
// },
|
|
}}
|
|
>
|
|
{/* Timeline line */}
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: 3,
|
|
bgcolor: 'neutral.outlinedBorder',
|
|
mt: 2,
|
|
}}
|
|
>
|
|
{/* Due date marker */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
left: '50%',
|
|
height: 16,
|
|
width: 3,
|
|
bgcolor: 'warning.500',
|
|
top: -8,
|
|
transform: 'translateX(-50%)',
|
|
borderRadius: 'sm',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Typography
|
|
level={'body-xs'}
|
|
sx={{
|
|
mt: 4,
|
|
fontWeight: 'md',
|
|
color: 'warning.700',
|
|
fontSize: '0.6rem',
|
|
}}
|
|
>
|
|
Due Date
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Notification markers */}
|
|
{sorted.map((n, i) => {
|
|
// Calculate position based on actual time duration
|
|
const percent = getPositionPercent(n.value, n.unit)
|
|
|
|
return (
|
|
<Box
|
|
key={i}
|
|
sx={{
|
|
position: 'absolute',
|
|
left: `${percent}%`,
|
|
transform: 'translateX(-50%)',
|
|
color:
|
|
convertToMinutes(n.value, n.unit) < 0
|
|
? TASK_COLOR.SCHEDULED
|
|
: convertToMinutes(n.value, n.unit) === 0
|
|
? TASK_COLOR.TODAY
|
|
: TASK_COLOR.LATE,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
transition: 'all 0.3s ease',
|
|
cursor: 'pointer',
|
|
opacity: 0.85,
|
|
'&:hover': {
|
|
opacity: 1,
|
|
transform: 'translateX(-50%) scale(1.1)',
|
|
zIndex: 10,
|
|
},
|
|
}}
|
|
title={getRelativeLabel(n)}
|
|
>
|
|
<Badge
|
|
badgeContent={
|
|
notificationIndexMap[
|
|
notifications.findIndex(
|
|
original =>
|
|
Number(original.value) === Number(n.value) &&
|
|
original.unit === n.unit,
|
|
)
|
|
] || i + 1
|
|
}
|
|
size={'sm'}
|
|
variant={'solid'}
|
|
sx={{
|
|
'--Badge-paddingX': '4px',
|
|
'--Badge-minHeight': '16px',
|
|
'--Badge-fontSize': '0.65rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
'& .MuiBadge-badge': {
|
|
background:
|
|
convertToMinutes(n.value, n.unit) < 0
|
|
? NOTIFICATION_TYPE.PREDUE
|
|
: convertToMinutes(n.value, n.unit) === 0
|
|
? NOTIFICATION_TYPE.DUE_DATE
|
|
: NOTIFICATION_TYPE.POSTDUE,
|
|
color: 'white',
|
|
},
|
|
}}
|
|
>
|
|
<NotificationsIcon
|
|
fontSize={'small'}
|
|
sx={{
|
|
height: 18,
|
|
width: 18,
|
|
}}
|
|
/>
|
|
</Badge>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
{error && (
|
|
<Alert
|
|
variant='soft'
|
|
color='danger'
|
|
sx={{ mb: 2 }}
|
|
startDecorator={<InfoIcon />}
|
|
>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{notifications
|
|
.map((n, idx) => ({ notification: n, originalIndex: idx }))
|
|
.sort((a, b) => {
|
|
const aBadgeNumber = notificationIndexMap[a.originalIndex] || 0
|
|
const bBadgeNumber = notificationIndexMap[b.originalIndex] || 0
|
|
return aBadgeNumber - bBadgeNumber
|
|
})
|
|
.map(({ notification: n, originalIndex: idx }) => {
|
|
// Get ordered badge number from timeline sorting
|
|
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 {
|
|
bgColor: NOTIFICATION_TYPE.PREDUE,
|
|
lightBg: `${NOTIFICATION_TYPE.PREDUE}20`,
|
|
borderColor: `${NOTIFICATION_TYPE.PREDUE}40`,
|
|
textColor: NOTIFICATION_TYPE.PREDUE,
|
|
}
|
|
} else if (Number(value) === 0) {
|
|
return {
|
|
bgColor: NOTIFICATION_TYPE.DUE_DATE,
|
|
lightBg: `${NOTIFICATION_TYPE.DUE_DATE}20`,
|
|
borderColor: `${NOTIFICATION_TYPE.DUE_DATE}40`,
|
|
textColor: NOTIFICATION_TYPE.DUE_DATE,
|
|
}
|
|
} else {
|
|
return {
|
|
bgColor: NOTIFICATION_TYPE.POSTDUE,
|
|
lightBg: `${NOTIFICATION_TYPE.POSTDUE}20`,
|
|
borderColor: `${NOTIFICATION_TYPE.POSTDUE}40`,
|
|
textColor: NOTIFICATION_TYPE.POSTDUE,
|
|
}
|
|
}
|
|
}
|
|
|
|
const colors = getNotificationColors(n.value)
|
|
|
|
return (
|
|
<Box key={idx} sx={{ position: 'relative' }}>
|
|
<Badge
|
|
badgeContent={badgeNumber}
|
|
size={'sm'}
|
|
sx={{
|
|
'--Badge-minHeight': '16px',
|
|
'--Badge-fontSize': '0.7rem',
|
|
'--Badge-paddingX': '5px',
|
|
top: 10,
|
|
left: 10,
|
|
'& .MuiBadge-badge': {
|
|
background: colors.bgColor,
|
|
color: 'white',
|
|
},
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
mb: 1.5,
|
|
p: 2,
|
|
borderRadius: 8,
|
|
border: '1px solid',
|
|
borderColor: 'neutral.outlinedBorder',
|
|
background: 'background.surface',
|
|
transition: 'all 0.2s ease',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1.5,
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
minWidth: 0,
|
|
flex: '1',
|
|
display: { xs: 'none', md: 'flex' }, // Show only on md and up
|
|
}}
|
|
>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: 'text.primary',
|
|
fontSize: 14,
|
|
}}
|
|
>
|
|
{getRelativeLabel(n)}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Select
|
|
value={uiRep.timing}
|
|
onChange={(_, value) => handleChange(idx, 'timing', value)}
|
|
onBlur={() => handleBlur(idx)}
|
|
sx={{ minWidth: 80 }}
|
|
size={'sm'}
|
|
>
|
|
{timingOptions.map(opt => (
|
|
<Option
|
|
key={opt.value}
|
|
value={opt.value}
|
|
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
|
|
>
|
|
{opt.label}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
<Input
|
|
type={'number'}
|
|
min={0}
|
|
value={
|
|
draftValues[idx] !== undefined
|
|
? draftValues[idx]
|
|
: uiRep.displayValue
|
|
}
|
|
disabled={uiRep.timing === 'ondue'}
|
|
onChange={e => {
|
|
const val = e.target.value
|
|
if (val.includes('-')) return
|
|
|
|
setDraftValues(prev => ({ ...prev, [idx]: val }))
|
|
}}
|
|
onKeyDown={e => {
|
|
if (['-', 'e', '+', '.'].includes(e.key)) {
|
|
e.preventDefault()
|
|
}
|
|
}}
|
|
onBlur={e => {
|
|
let val = e.target.value
|
|
|
|
const numericVal = Number(val)
|
|
val =
|
|
numericVal <= 0
|
|
? hasOnDueElsewhere
|
|
? 1
|
|
: 0
|
|
: numericVal
|
|
|
|
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,
|
|
}}
|
|
size={'sm'}
|
|
placeholder='0'
|
|
/>
|
|
<Select
|
|
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,
|
|
}}
|
|
size={'sm'}
|
|
>
|
|
{timeUnits.map(opt => (
|
|
<Option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
<IconButton
|
|
onClick={() => removeNotification(idx)}
|
|
disabled={notifications.length === 1}
|
|
color={'danger'}
|
|
size={'sm'}
|
|
variant={'soft'}
|
|
sx={{
|
|
transition: 'all 0.2s ease',
|
|
'&:hover': {
|
|
transform: 'scale(1.1)',
|
|
},
|
|
}}
|
|
>
|
|
<DeleteIcon fontSize={'small'} />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
})}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: 1.5,
|
|
mt: 1,
|
|
mb: 2,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<Button
|
|
onClick={() => addSmartNotification('reminder')}
|
|
disabled={notifications.length >= maxNotifications}
|
|
startDecorator={<AddIcon />}
|
|
size={'sm'}
|
|
variant={'outlined'}
|
|
sx={{
|
|
borderRadius: 6,
|
|
fontWeight: 500,
|
|
borderColor: `${TASK_COLOR.SCHEDULED}60`,
|
|
color: TASK_COLOR.SCHEDULED,
|
|
'&:hover': {
|
|
borderColor: TASK_COLOR.SCHEDULED,
|
|
background: `${TASK_COLOR.SCHEDULED}10`,
|
|
},
|
|
}}
|
|
>
|
|
Reminder
|
|
</Button>
|
|
<Button
|
|
onClick={() => addSmartNotification('due')}
|
|
disabled={
|
|
notifications.length >= maxNotifications ||
|
|
notifications.some(n => Number(n.value) === 0)
|
|
}
|
|
startDecorator={<AddIcon />}
|
|
size={'sm'}
|
|
variant={'outlined'}
|
|
sx={{
|
|
borderRadius: 6,
|
|
fontWeight: 500,
|
|
borderColor: `${NOTIFICATION_TYPE.DUE_DATE}60`,
|
|
color: NOTIFICATION_TYPE.DUE_DATE,
|
|
'&:hover': {
|
|
borderColor: NOTIFICATION_TYPE.DUE_DATE,
|
|
background: `${NOTIFICATION_TYPE.DUE_DATE}10`,
|
|
},
|
|
}}
|
|
>
|
|
Due Alert
|
|
</Button>
|
|
<Button
|
|
onClick={() => addSmartNotification('followup')}
|
|
disabled={notifications.length >= maxNotifications}
|
|
startDecorator={<AddIcon />}
|
|
size={'sm'}
|
|
variant={'outlined'}
|
|
sx={{
|
|
borderRadius: 6,
|
|
fontWeight: 500,
|
|
borderColor: `${NOTIFICATION_TYPE.POSTDUE}60`,
|
|
color: NOTIFICATION_TYPE.POSTDUE,
|
|
'&:hover': {
|
|
borderColor: NOTIFICATION_TYPE.POSTDUE,
|
|
background: `${NOTIFICATION_TYPE.POSTDUE}10`,
|
|
},
|
|
}}
|
|
>
|
|
Follow-up
|
|
</Button>
|
|
</Box>
|
|
{showSaveDefault && (
|
|
<Box
|
|
sx={{
|
|
mt: 1,
|
|
display: 'flex',
|
|
justifyContent: 'start',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
}}
|
|
>
|
|
<Button
|
|
variant='outlined'
|
|
size='sm'
|
|
color='neutral'
|
|
startDecorator={<Save />}
|
|
sx={{
|
|
borderRadius: 6,
|
|
fontWeight: 500,
|
|
'&:hover': {
|
|
background: 'neutral.softHoverBg',
|
|
},
|
|
}}
|
|
onClick={() => {
|
|
localStorage.setItem(
|
|
'defaultNotificationTemplate',
|
|
JSON.stringify(notifications),
|
|
)
|
|
setShowSaveDefault(false)
|
|
}}
|
|
>
|
|
Remember for Future Tasks
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{showTimeline && renderTimeline()}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export default NotificationTemplate
|