Merge branch 'dev'
This commit is contained in:
29
src/App.jsx
29
src/App.jsx
@@ -7,12 +7,10 @@ import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||
import { UserContext } from './contexts/UserContext'
|
||||
import { useResource } from './queries/ResourceQueries'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
import { ErrorProvider } from './service/ErrorProvider'
|
||||
import { GetUserProfile } from './utils/Fetcher'
|
||||
import { apiManager, isTokenValid } from './utils/TokenManager'
|
||||
import { apiManager } from './utils/TokenManager'
|
||||
import NetworkBanner from './views/components/NetworkBanner'
|
||||
const add = className => {
|
||||
document.getElementById('root').classList.add(className)
|
||||
@@ -23,15 +21,14 @@ const remove = className => {
|
||||
}
|
||||
// TODO: Update the interval to at 60 minutes
|
||||
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
const queryClient = new QueryClient({})
|
||||
function App() {
|
||||
const resource = useResource()
|
||||
const navigate = useNavigate()
|
||||
startApiManager(navigate)
|
||||
startOpenReplay()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
const [userProfile, setUserProfile] = useState(null)
|
||||
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true)
|
||||
|
||||
const {
|
||||
@@ -72,23 +69,12 @@ function App() {
|
||||
|
||||
return remove('dark')
|
||||
}
|
||||
const getUserProfile = () => {
|
||||
GetUserProfile()
|
||||
.then(res => {
|
||||
res.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
})
|
||||
})
|
||||
.catch(error => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setThemeClass()
|
||||
}, [mode, systemMode])
|
||||
useEffect(() => {
|
||||
registerCapacitorListeners()
|
||||
if (isTokenValid()) {
|
||||
if (!userProfile) getUserProfile()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -99,10 +85,8 @@ function App() {
|
||||
<AuthenticationProvider />
|
||||
<ErrorProvider>
|
||||
<ImpersonateUserProvider>
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</ImpersonateUserProvider>
|
||||
</ErrorProvider>
|
||||
|
||||
@@ -133,6 +117,7 @@ const startOpenReplay = () => {
|
||||
const tracker = new Tracker({
|
||||
projectKey: import.meta.env.VITE_OPENREPLAY_PROJECT_KEY,
|
||||
})
|
||||
|
||||
tracker.start()
|
||||
}
|
||||
export default App
|
||||
|
||||
641
src/components/NotificationTemplate.jsx
Normal file
641
src/components/NotificationTemplate.jsx
Normal file
@@ -0,0 +1,641 @@
|
||||
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, useState } from 'react'
|
||||
|
||||
const timeUnits = [
|
||||
{ label: 'Mins', value: 'minutes' },
|
||||
{ label: 'Hours', value: 'hours' },
|
||||
{ label: 'Days', value: 'days' },
|
||||
]
|
||||
|
||||
const beforeAfterOptions = [
|
||||
{ label: 'Before', value: 'before' },
|
||||
{ label: 'On Due', value: 'ondue' },
|
||||
{ label: 'After', value: 'after' },
|
||||
]
|
||||
|
||||
function getRelativeLabel(notification) {
|
||||
const { value, unit, type } = notification
|
||||
if (type === 'ondue') {
|
||||
return 'On due date'
|
||||
}
|
||||
return `${value} ${unit} ${type === 'before' ? 'before' : 'after'} due`
|
||||
}
|
||||
|
||||
const NotificationTemplate = ({
|
||||
maxNotifications = 5,
|
||||
onChange,
|
||||
value,
|
||||
showTimeline = true,
|
||||
}) => {
|
||||
const [templateName, setTemplateName] = useState(
|
||||
value?.name || 'New Notification Template',
|
||||
)
|
||||
const [notifications, setNotifications] = useState(
|
||||
value?.templates ||
|
||||
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
||||
[],
|
||||
)
|
||||
|
||||
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(() => {
|
||||
// Sort notifications for consistent ordering
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
// Always ensure correct ordering: Before Due -> On Due -> After Due
|
||||
if (a.type !== b.type) {
|
||||
// Before Due first
|
||||
if (a.type === 'before') return -1
|
||||
if (b.type === 'before') return 1
|
||||
|
||||
// On Due comes before After Due
|
||||
if (a.type === 'ondue') return -1
|
||||
if (b.type === 'ondue') return 1
|
||||
// DEFAULT CASE ( NOT SURE FOR FUTURE )?
|
||||
return 0
|
||||
}
|
||||
|
||||
const getMinutes = notif => {
|
||||
const { value, unit } = notif
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return minutes
|
||||
}
|
||||
|
||||
// For Before Due: sort in descending order (furthest from due first)
|
||||
// For After Due: sort in ascending order (closest to due first)
|
||||
const aMinutes = getMinutes(a)
|
||||
const bMinutes = getMinutes(b)
|
||||
return a.type === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
|
||||
})
|
||||
|
||||
const indexMap = {}
|
||||
sorted.forEach((item, index) => {
|
||||
const originalIdx = notifications.findIndex(
|
||||
n =>
|
||||
n.value === item.value &&
|
||||
n.unit === item.unit &&
|
||||
n.type === item.type,
|
||||
)
|
||||
indexMap[originalIdx] = index + 1
|
||||
})
|
||||
|
||||
setNotificationIndexMap(indexMap)
|
||||
}, [notifications])
|
||||
|
||||
// Sort notifications and update the index mapping
|
||||
useEffect(() => {
|
||||
updateNotificationIndices()
|
||||
setError(null)
|
||||
}, [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) => {
|
||||
return notifications.some((n, idx) => {
|
||||
if (idx === currentIdx) return false
|
||||
|
||||
return (
|
||||
n.value === notification.value &&
|
||||
n.unit === notification.unit &&
|
||||
n.type === notification.type
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (idx, field, value) => {
|
||||
let updatedNotification = {
|
||||
...notifications[idx],
|
||||
[field]: value,
|
||||
}
|
||||
|
||||
// Special handling for "On Due" option
|
||||
if (field === 'type' && value === 'ondue') {
|
||||
// Set default values for On Due (not applicable)
|
||||
updatedNotification = {
|
||||
...updatedNotification,
|
||||
value: 1,
|
||||
unit: 'minutes',
|
||||
}
|
||||
|
||||
// Check if another notification is already "On Due"
|
||||
const existingOnDue = notifications.findIndex(
|
||||
(n, i) => i !== idx && n.type === 'ondue',
|
||||
)
|
||||
|
||||
if (existingOnDue !== -1) {
|
||||
setError(
|
||||
'Only one notification can be set to "On Due". Please choose a different timing.',
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isDuplicate(updatedNotification, idx)) {
|
||||
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: 'hours', type: 'before' },
|
||||
{ value: 1, unit: 'days', type: 'before' },
|
||||
{ value: 30, unit: 'minutes', type: 'before' },
|
||||
{ value: 2, unit: 'hours', type: 'before' },
|
||||
{ value: 3, unit: 'days', type: 'before' },
|
||||
]
|
||||
break
|
||||
|
||||
case 'due':
|
||||
if (notifications.some(n => n.type === 'ondue')) {
|
||||
setError('Only one "Due Alert" notification is allowed.')
|
||||
return
|
||||
}
|
||||
newNotification = { value: 0, unit: 'minutes', type: 'ondue' }
|
||||
break
|
||||
|
||||
case 'followup':
|
||||
suggestions = [
|
||||
{ value: 1, unit: 'hours', type: 'after' },
|
||||
{ value: 1, unit: 'days', type: 'after' },
|
||||
{ value: 3, unit: 'days', type: 'after' },
|
||||
{ value: 1, unit: 'weeks', type: 'after' },
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
// For reminder/followup, find first non-duplicate suggestion
|
||||
if (suggestions.length > 0) {
|
||||
newNotification = suggestions.find(suggestion => !isDuplicate(suggestion))
|
||||
|
||||
if (!newNotification) {
|
||||
setError(`All common ${type} times are already configured.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the new notification in the correct chronological position
|
||||
const updatedNotifications = [...notifications, newNotification].sort(
|
||||
(a, b) => {
|
||||
// Convert everything to minutes for consistent comparison
|
||||
const getMinutes = notif => {
|
||||
const { value, unit, type } = notif
|
||||
// On Due is exactly at due date (0 minutes)
|
||||
if (type === 'ondue') return 0
|
||||
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return type === 'before' ? -minutes : minutes
|
||||
}
|
||||
|
||||
return getMinutes(a) - getMinutes(b)
|
||||
},
|
||||
)
|
||||
|
||||
setNotifications(updatedNotifications)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const removeNotification = idx => {
|
||||
const updated = notifications.filter((_, i) => i !== idx)
|
||||
setNotifications(updated)
|
||||
onChange && onChange(updated)
|
||||
}
|
||||
const renderTimeline = () => {
|
||||
// Sort notifications chronologically
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
// Convert everything to minutes for consistent comparison
|
||||
const getMinutes = notif => {
|
||||
const { value, unit, type } = notif
|
||||
// On Due is exactly at due date (0 minutes)
|
||||
if (type === 'ondue') return 0
|
||||
|
||||
let minutes = value
|
||||
if (unit === 'hours') minutes *= 60
|
||||
if (unit === 'days') minutes *= 24 * 60
|
||||
return type === 'before' ? -minutes : minutes
|
||||
}
|
||||
|
||||
return getMinutes(a) - getMinutes(b)
|
||||
})
|
||||
|
||||
// Create a map to track sorted indices for original notifications
|
||||
const notificationIndexMap = {}
|
||||
sorted.forEach((item, index) => {
|
||||
// Find the original index of this item in notifications array
|
||||
const originalIdx = notifications.findIndex(
|
||||
n =>
|
||||
n.value === item.value &&
|
||||
n.unit === item.unit &&
|
||||
n.type === item.type,
|
||||
)
|
||||
notificationIndexMap[originalIdx] = index + 1
|
||||
}) // Get min and max notification times for dynamic scaling
|
||||
const minutesValues = sorted.map(n => {
|
||||
if (n.type === 'ondue') return 0
|
||||
|
||||
let minutes = n.value
|
||||
if (n.unit === 'hours') minutes *= 60
|
||||
if (n.unit === 'days') minutes *= 24 * 60
|
||||
return n.type === 'before' ? -minutes : minutes
|
||||
})
|
||||
|
||||
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 = minutes => {
|
||||
// Due date is always at center (50%)
|
||||
if (minutes === 0) return 50
|
||||
|
||||
// For notifications before due date
|
||||
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
|
||||
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.level1',
|
||||
borderRadius: 'md',
|
||||
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) => {
|
||||
// Convert to minutes for consistent scale
|
||||
let minutes = 0
|
||||
if (n.type !== 'ondue') {
|
||||
minutes = n.value
|
||||
if (n.unit === 'hours') minutes *= 60
|
||||
if (n.unit === 'days') minutes *= 24 * 60
|
||||
if (n.type === 'before') minutes = -minutes
|
||||
}
|
||||
// On Due notifications are always at the due date (0 minutes)
|
||||
|
||||
// Calculate position based on dynamic scaling
|
||||
const percent = getPositionPercent(minutes)
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: `${percent}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
color:
|
||||
n.type === 'before'
|
||||
? 'primary.600'
|
||||
: n.type === 'ondue'
|
||||
? 'warning.600'
|
||||
: 'success.600',
|
||||
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={i + 1}
|
||||
size={'sm'}
|
||||
variant={'solid'}
|
||||
color={
|
||||
n.type === 'before'
|
||||
? 'success'
|
||||
: n.type === 'ondue'
|
||||
? 'warning'
|
||||
: 'danger'
|
||||
}
|
||||
sx={{
|
||||
'--Badge-paddingX': '4px',
|
||||
'--Badge-minHeight': '16px',
|
||||
'--Badge-fontSize': '0.65rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon
|
||||
fontSize={'small'}
|
||||
sx={{
|
||||
height: 18,
|
||||
width: 18,
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={
|
||||
{
|
||||
// border: '1px solid',
|
||||
// borderColor: 'neutral.outlinedBorder',
|
||||
// borderRadius: 2,
|
||||
// p: 3,
|
||||
// maxWidth: 500,
|
||||
// bgcolor: 'background.body',
|
||||
// boxShadow: 'sm',
|
||||
}
|
||||
}
|
||||
>
|
||||
{/* <Typography level={'h4'} sx={{ mb: 2 }}>
|
||||
Schedule Name
|
||||
</Typography> */}
|
||||
|
||||
{/* Template Name Field */}
|
||||
{/* <Box sx={{ mb: 3 }}>
|
||||
<Typography level={'body2'} sx={{ mb: 1, fontWeight: 'md' }}>
|
||||
Template Name
|
||||
</Typography>
|
||||
<Input
|
||||
value={templateName}
|
||||
onChange={handleNameChange}
|
||||
placeholder='Enter template name'
|
||||
sx={{ width: '100%' }}
|
||||
/>
|
||||
</Box> */}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
variant='soft'
|
||||
color='danger'
|
||||
sx={{ mb: 2 }}
|
||||
startDecorator={<InfoIcon />}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{notifications.map((n, idx) => {
|
||||
// Get ordered badge number from timeline sorting
|
||||
const badgeNumber = notificationIndexMap[idx]
|
||||
|
||||
return (
|
||||
<Box key={idx} sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
width: 18,
|
||||
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={badgeNumber}
|
||||
size={'sm'}
|
||||
sx={{
|
||||
'--Badge-minHeight': '20px',
|
||||
'--Badge-fontSize': '0.75rem',
|
||||
// centering the badge:
|
||||
}}
|
||||
color={
|
||||
n.type === 'before'
|
||||
? 'success'
|
||||
: n.type === 'ondue'
|
||||
? 'warning'
|
||||
: 'danger'
|
||||
}
|
||||
>
|
||||
{/* Empty box to attach badge to */}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Select
|
||||
value={n.type}
|
||||
onChange={(_, value) => handleChange(idx, 'type', value)}
|
||||
sx={{ mr: 1, minWidth: 100 }}
|
||||
size={'sm'}
|
||||
>
|
||||
{beforeAfterOptions.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{/* Show disabled fields for "On Due" option for visual consistency */}
|
||||
<Input
|
||||
type={'number'}
|
||||
min={1}
|
||||
disabled={n.type === 'ondue'}
|
||||
value={n.type === 'ondue' ? '—' : n.value}
|
||||
onChange={e =>
|
||||
handleChange(idx, 'value', Math.max(1, Number(e.target.value)))
|
||||
}
|
||||
sx={{
|
||||
width: 70,
|
||||
mr: 1,
|
||||
opacity: n.type === 'ondue' ? 0.6 : 1,
|
||||
...(n.type === 'ondue' && {
|
||||
'& input': {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
size={'sm'}
|
||||
placeholder={n.type === 'ondue' ? '—' : ''}
|
||||
/>
|
||||
<Select
|
||||
value={n.unit}
|
||||
disabled={n.type === 'ondue'}
|
||||
onChange={(_, value) => handleChange(idx, 'unit', value)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
minWidth: 80,
|
||||
opacity: n.type === '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'}
|
||||
sx={{ mr: 1 }}
|
||||
variant={'soft'}
|
||||
>
|
||||
<DeleteIcon fontSize={'small'} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 1, mb: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('reminder')}
|
||||
disabled={notifications.length >= maxNotifications}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
color={'primary'}
|
||||
>
|
||||
Reminder
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('due')}
|
||||
disabled={
|
||||
notifications.length >= maxNotifications ||
|
||||
notifications.some(n => n.type === 'ondue')
|
||||
}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
color={'warning'}
|
||||
>
|
||||
Due Alert
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('followup')}
|
||||
disabled={notifications.length >= maxNotifications}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
color={'danger'}
|
||||
>
|
||||
Follow-up
|
||||
</Button>
|
||||
</Box>
|
||||
{showSaveDefault && (
|
||||
<Button
|
||||
variant='outlined'
|
||||
size='sm'
|
||||
color='neutral'
|
||||
// sx={{ ml: 'auto', mt: 0.5 }}
|
||||
startDecorator={<Save />}
|
||||
onClick={() => {
|
||||
localStorage.setItem(
|
||||
'defaultNotificationTemplate',
|
||||
JSON.stringify(notifications),
|
||||
)
|
||||
setShowSaveDefault(false)
|
||||
}}
|
||||
>
|
||||
Save as Default for Future Tasks
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showTimeline && renderTimeline()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationTemplate
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
const UserContext = createContext({
|
||||
userProfile: null,
|
||||
setUserProfile: () => {},
|
||||
})
|
||||
|
||||
export { UserContext }
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
||||
import {
|
||||
GetAllCircleMembers,
|
||||
GetAllUsers,
|
||||
GetUserProfile,
|
||||
} from '../utils/Fetcher'
|
||||
import { isTokenValid } from '../utils/TokenManager'
|
||||
|
||||
export const useAllUsers = () => {
|
||||
return useQuery({
|
||||
@@ -22,3 +27,27 @@ export const useCircleMembers = () => {
|
||||
|
||||
return { data, error, isLoading, handleRefetch }
|
||||
}
|
||||
|
||||
export const useUserProfile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['userProfile'],
|
||||
queryFn: async () => {
|
||||
const resp = await GetUserProfile()
|
||||
const result = await resp.json()
|
||||
if (!isTokenValid()) {
|
||||
return null // Token is invalid, return null to indicate no profile
|
||||
}
|
||||
return result.res // Return the actual user profile data
|
||||
},
|
||||
staleTime: 30 * 60 * 1000, // 30 minutes in milliseconds
|
||||
gcTime: 30 * 60 * 1000, // 30 minutes in milliseconds
|
||||
})
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
refetch: () => queryClient.invalidateQueries(['userProfile']),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,16 @@ export async function UploadFile(url, options) {
|
||||
export async function Fetch(url, options) {
|
||||
if (!isTokenValid()) {
|
||||
Cookies.set('ca_redirect', window.location.pathname)
|
||||
window.location.href = '/login'
|
||||
if (!window.location.pathname === '/login') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
// clone options to avoid mutation
|
||||
const cacheKey = { ...options }
|
||||
options.headers = { ...options.headers, ...HEADERS() }
|
||||
|
||||
const baseURL = apiManager.getApiURL()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Logo from '../../Logo'
|
||||
import { apiManager } from '../../utils/TokenManager'
|
||||
|
||||
import Cookies from 'js-cookie'
|
||||
import { useRef } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { GetUserProfile } from '../../utils/Fetcher'
|
||||
|
||||
const AuthenticationLoading = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
|
||||
const Navigate = useNavigate()
|
||||
const hasCalledHandleOAuth2 = useRef(false)
|
||||
const [message, setMessage] = useState('Authenticating')
|
||||
@@ -29,15 +29,16 @@ const AuthenticationLoading = () => {
|
||||
const getUserProfileAndNavigateToHome = () => {
|
||||
GetUserProfile().then(data => {
|
||||
data.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
// check if redirect url is set in cookie:
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
refetchUserProfile.then(() => {
|
||||
// check if redirect url is set in cookie:
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,24 +16,26 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import Cookies from 'js-cookie'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { LoginSocialGoogle } from 'reactjs-social-login'
|
||||
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import Logo from '../../Logo'
|
||||
import { useResource } from '../../queries/ResourceQueries'
|
||||
import { GetUserProfile, login } from '../../utils/Fetcher'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { login } from '../../utils/Fetcher'
|
||||
import { apiManager } from '../../utils/TokenManager'
|
||||
import MFAVerificationModal from './MFAVerificationModal'
|
||||
|
||||
const LoginView = () => {
|
||||
const { userProfile, setUserProfile } = React.useContext(UserContext)
|
||||
const [username, setUsername] = React.useState('')
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [error, setError] = React.useState(null)
|
||||
const [mfaModalOpen, setMfaModalOpen] = React.useState(false)
|
||||
const [mfaSessionToken, setMfaSessionToken] = React.useState('')
|
||||
// Only fetch user profile if token is valid to prevent unnecessary queries
|
||||
const { data: userProfileData } = useUserProfile()
|
||||
const [userProfile, setUserProfile] = useState(null)
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
const [mfaModalOpen, setMfaModalOpen] = useState(false)
|
||||
const [mfaSessionToken, setMfaSessionToken] = useState('')
|
||||
const { data: resource } = useResource()
|
||||
const Navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
@@ -136,19 +138,17 @@ const LoginView = () => {
|
||||
})
|
||||
}
|
||||
const getUserProfileAndNavigateToHome = () => {
|
||||
GetUserProfile().then(data => {
|
||||
data.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
// check if redirect url is set in cookie:
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
})
|
||||
})
|
||||
// Refetch user profile after login
|
||||
// refetchUserProfile().then(() => {
|
||||
// // check if redirect url is set in cookie:
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
// })
|
||||
}
|
||||
|
||||
const handleMFASuccess = data => {
|
||||
@@ -274,7 +274,6 @@ const LoginView = () => {
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
q
|
||||
variant='plain'
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -283,7 +282,6 @@ const LoginView = () => {
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
setUserProfile(null)
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
// go to login page:
|
||||
@@ -353,7 +351,6 @@ const LoginView = () => {
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
q
|
||||
variant='plain'
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -382,7 +379,7 @@ const LoginView = () => {
|
||||
onResolve={({ provider, data }) => {
|
||||
loggedWithProvider(provider, data)
|
||||
}}
|
||||
onReject={err => {
|
||||
onReject={() => {
|
||||
setError("Couldn't log in with Google, please try again")
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -24,14 +24,15 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import NotificationTemplate from '../../components/NotificationTemplate.jsx'
|
||||
import {
|
||||
useChore,
|
||||
useCreateChore,
|
||||
useUpdateChore,
|
||||
} from '../../queries/ChoreQueries.jsx'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
DeleteChore,
|
||||
@@ -61,7 +62,8 @@ const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
|
||||
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
|
||||
const NO_DUE_DATE_ALLOWED_TYPE = ['trigger']
|
||||
const ChoreEdit = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [chore, setChore] = useState([])
|
||||
const [choresHistory, setChoresHistory] = useState([])
|
||||
const [userHistory, setUserHistory] = useState({})
|
||||
@@ -708,6 +710,10 @@ const ChoreEdit = () => {
|
||||
<Checkbox
|
||||
onChange={e => {
|
||||
setIsNotificable(e.target.checked)
|
||||
// if unchecking, reset notification metadata:
|
||||
if (!e.target.checked) {
|
||||
setNotificationMetadata({})
|
||||
}
|
||||
}}
|
||||
defaultChecked={isNotificable}
|
||||
checked={isNotificable}
|
||||
@@ -726,7 +732,6 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
{isNotificable && (
|
||||
<Box
|
||||
ml={4}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -736,58 +741,20 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
<Card variant='outlined'>
|
||||
<Typography level='h5'>
|
||||
What things should trigger the notification?
|
||||
</Typography>
|
||||
{[
|
||||
{
|
||||
title: 'Due Date/Time',
|
||||
description: 'A simple reminder that a task is due',
|
||||
id: 'dueDate',
|
||||
},
|
||||
// {
|
||||
// title: 'Upon Completion',
|
||||
// description: 'A notification when a task is completed',
|
||||
// id: 'completion',
|
||||
// },
|
||||
{
|
||||
title: 'Predued',
|
||||
description: 'before a task is due in few hours',
|
||||
id: 'predue',
|
||||
},
|
||||
// {
|
||||
// title: 'Overdue',
|
||||
// description: 'A notification when a task is overdue',
|
||||
// id: 'overdue',
|
||||
// },
|
||||
{
|
||||
title: 'Nagging',
|
||||
description: 'Daily reminders until the task is completed',
|
||||
id: 'nagging',
|
||||
},
|
||||
].map(item => (
|
||||
<FormControl sx={{ mb: 1 }} key={item.id}>
|
||||
<Checkbox
|
||||
overlay
|
||||
onClick={() => {
|
||||
setNotificationMetadata({
|
||||
...notificationMetadata,
|
||||
[item.id]: !notificationMetadata[item.id],
|
||||
})
|
||||
}}
|
||||
checked={
|
||||
notificationMetadata ? notificationMetadata[item.id] : false
|
||||
<Typography level='body-md'>Notification Schedule:</Typography>
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<NotificationTemplate
|
||||
onChange={metadata => {
|
||||
const newNotificaitonMetadata = {
|
||||
...notificationMetadata,
|
||||
templates: metadata.notifications,
|
||||
}
|
||||
label={item.title}
|
||||
key={item.title}
|
||||
/>
|
||||
<FormHelperText>{item.description}</FormHelperText>
|
||||
</FormControl>
|
||||
))}
|
||||
|
||||
<Typography level='h5'>
|
||||
What things should trigger the notification?
|
||||
</Typography>
|
||||
setNotificationMetadata(newNotificaitonMetadata)
|
||||
}}
|
||||
value={notificationMetadata}
|
||||
/>
|
||||
</Box>
|
||||
<Typography level='h5'>Choose Who to Notify:</Typography>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
overlay
|
||||
@@ -856,7 +823,7 @@ const ChoreEdit = () => {
|
||||
onChange={(event, newValue) => {
|
||||
setLabelsV2(userLabels.filter(l => newValue.indexOf(l.name) > -1))
|
||||
}}
|
||||
value={labelsV2.map(l => l.name)}
|
||||
value={labelsV2?.map(l => l.name)}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: '0.25rem' }}>
|
||||
{labelsV2.map(selectedOption => {
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import ThingTriggerSection from './ThingTriggerSection'
|
||||
|
||||
@@ -68,7 +69,6 @@ const RepeatOnSections = ({
|
||||
frequencyMetadata,
|
||||
onFrequencyMetadataUpdate,
|
||||
}) => {
|
||||
const [intervalUnit, setIntervalUnit] = useState('days')
|
||||
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
|
||||
// otherwise set it to 18:00 of the current day
|
||||
useEffect(() => {
|
||||
@@ -117,13 +117,16 @@ const RepeatOnSections = ({
|
||||
onFrequencyUpdate(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Select placeholder='Unit' value={intervalUnit}>
|
||||
<Select
|
||||
placeholder='Unit'
|
||||
value={frequencyMetadata?.unit || 'days'}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{['hours', 'days', 'weeks', 'months', 'years'].map(item => (
|
||||
<Option
|
||||
key={item}
|
||||
value={item}
|
||||
onClick={() => {
|
||||
setIntervalUnit(item)
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
unit: item,
|
||||
@@ -337,7 +340,8 @@ const RepeatSection = ({
|
||||
isAttemptToSave,
|
||||
selectedThing,
|
||||
}) => {
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Typography level='h4'>Repeat :</Typography>
|
||||
|
||||
@@ -22,7 +22,7 @@ import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
@@ -63,7 +63,8 @@ const ChoreCard = ({
|
||||
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError } = useError()
|
||||
|
||||
@@ -18,7 +18,7 @@ import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
@@ -60,7 +60,8 @@ const CompactChoreCard = ({
|
||||
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError } = useError()
|
||||
@@ -577,10 +578,11 @@ const CompactChoreCard = ({
|
||||
onDelete={handleDelete}
|
||||
sx={{
|
||||
width: 28,
|
||||
marginRight: -3,
|
||||
height: 28,
|
||||
// opacity: 0.6,
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
opacity: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,9 +30,8 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { GetArchivedChores } from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
@@ -42,7 +41,7 @@ import ChoreCard from './ChoreCard'
|
||||
import CompactChoreCard from './CompactChoreCard'
|
||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||
import TaskInput from '../components/AddTaskModal'
|
||||
import {
|
||||
@@ -54,7 +53,8 @@ import Sidepanel from './Sidepanel'
|
||||
import SortAndGrouping from './SortAndGrouping'
|
||||
|
||||
const MyChores = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||
useUserProfile()
|
||||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||
const [snackBarMessage, setSnackBarMessage] = useState(null)
|
||||
const [chores, setChores] = useState([])
|
||||
@@ -119,7 +119,7 @@ const MyChores = () => {
|
||||
scheduleChoreNotification(choresData.res, userProfile, membersData.res)
|
||||
}
|
||||
}
|
||||
}, [membersLoading, choresLoading, userProfile])
|
||||
}, [membersLoading, choresLoading, isUserProfileLoading])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
@@ -352,12 +352,39 @@ const MyChores = () => {
|
||||
}
|
||||
|
||||
if (
|
||||
userProfile === null ||
|
||||
isUserProfileLoading ||
|
||||
userLabelsLoading ||
|
||||
performers.length === 0 ||
|
||||
choresLoading
|
||||
) {
|
||||
return <LoadingComponent />
|
||||
console.log(
|
||||
'userProfile:',
|
||||
userProfile,
|
||||
'userLabelsLoading:',
|
||||
userLabelsLoading,
|
||||
'performers:',
|
||||
performers.length,
|
||||
'choresLoading:',
|
||||
choresLoading,
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{JSON.stringify(userProfile) === 'null'}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{userLabelsLoading}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{performers.length === 0}
|
||||
</Typography>
|
||||
<Typography level='title-lg' sx={{ mt: 2, mb: 2 }}>
|
||||
{choresLoading}
|
||||
</Typography>
|
||||
<LoadingComponent />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -55,7 +55,7 @@ const Sidepanel = ({ chores }) => {
|
||||
width: '315px',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: '100%', overflowY: 'hidden' }}>
|
||||
<Box sx={{ width: '100%', overflowY: 'hidden', overflowX: 'hidden' }}>
|
||||
<CalendarView chores={chores} />
|
||||
</Box>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Person } from '@mui/icons-material'
|
||||
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
||||
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import UserModal from '../Modals/Inputs/UserModal'
|
||||
const WelcomeCard = () => {
|
||||
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Box, Container, Input, Sheet, Typography } from '@mui/joy'
|
||||
import Logo from '../../Logo'
|
||||
|
||||
import { Button } from '@mui/joy'
|
||||
import { useContext } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { JoinCircle } from '../../utils/Fetcher'
|
||||
const JoinCircleView = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
let [searchParams, setSearchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const code = searchParams.get('code')
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import {
|
||||
CreateLongLiveToken,
|
||||
DeleteLongLiveToken,
|
||||
@@ -21,10 +22,10 @@ import { isPlusAccount } from '../../utils/Helpers'
|
||||
import TextModal from '../Modals/Inputs/TextModal'
|
||||
|
||||
const APITokenSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const [tokens, setTokens] = useState([])
|
||||
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
||||
const [showTokenId, setShowTokenId] = useState(null)
|
||||
const { userProfile } = useContext(UserContext)
|
||||
useEffect(() => {
|
||||
GetLongLiveTokens().then(resp => {
|
||||
resp.json().then(data => {
|
||||
|
||||
@@ -468,11 +468,6 @@ const MFASettings = () => {
|
||||
fontSize: '1.2em',
|
||||
letterSpacing: verificationCode.length === 0 ? '' : '0.4em',
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && verificationCode.length === 6) {
|
||||
handleDisableMFA()
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
maxLength: 6,
|
||||
|
||||
@@ -18,27 +18,18 @@ import {
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import {
|
||||
GetUserProfile,
|
||||
UpdateNotificationTarget,
|
||||
UpdateUserDetails,
|
||||
} from '../../utils/Fetcher'
|
||||
|
||||
const NotificationSetting = () => {
|
||||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
useEffect(() => {
|
||||
if (!userProfile) {
|
||||
GetUserProfile().then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
setChatID(data.res.chatID)
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
|
||||
|
||||
const getNotificationPreferences = async () => {
|
||||
const ret = await Preferences.get({ key: 'notificationPreferences' })
|
||||
return JSON.parse(ret.value)
|
||||
@@ -134,13 +125,7 @@ const NotificationSetting = () => {
|
||||
return
|
||||
}
|
||||
|
||||
setUserProfile({
|
||||
...userProfile,
|
||||
notification_target: {
|
||||
target: chatID,
|
||||
type: Number(notificationTarget),
|
||||
},
|
||||
})
|
||||
refetchUserProfile()
|
||||
alert('Notification target updated')
|
||||
})
|
||||
}
|
||||
@@ -339,7 +324,7 @@ const NotificationSetting = () => {
|
||||
chatID: Number(0),
|
||||
}).then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUserProfile(data)
|
||||
refetchUserProfile()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,16 +11,16 @@ import {
|
||||
} from '@mui/joy'
|
||||
import Modal from '@mui/joy/Modal'
|
||||
import ModalDialog from '@mui/joy/ModalDialog'
|
||||
import { useContext, useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import Cropper from 'react-easy-crop'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { UpdateUserDetails } from '../../utils/Fetcher'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||
import { getCroppedImg } from '../../utils/imageCropUtils'
|
||||
import { UploadFile } from '../../utils/TokenManager'
|
||||
|
||||
const ProfileSettings = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const [displayName, setDisplayName] = useState(userProfile?.displayName || '')
|
||||
const [timezone, setTimezone] = useState(
|
||||
userProfile?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
@@ -75,7 +75,6 @@ const ProfileSettings = () => {
|
||||
const url = resolvePhotoURL(data.url || data.sign)
|
||||
|
||||
setPhotoURL(url)
|
||||
setUserProfile({ ...userProfile, image: url })
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: 'Profile photo updated!',
|
||||
@@ -101,7 +100,6 @@ const ProfileSettings = () => {
|
||||
const response = await UpdateUserDetails(userDetails)
|
||||
|
||||
if (response.ok) {
|
||||
setUserProfile({ ...userProfile, displayName, timezone })
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: 'Profile updated successfully!',
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Logo from '../../Logo'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import {
|
||||
AcceptCircleMemberRequest,
|
||||
CancelSubscription,
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
GetCircleMemberRequests,
|
||||
GetSubscriptionSession,
|
||||
GetUserCircle,
|
||||
GetUserProfile,
|
||||
JoinCircle,
|
||||
LeaveCircle,
|
||||
PutWebhookURL,
|
||||
@@ -44,7 +43,8 @@ import StorageSettings from './StorageSettings'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
const Settings = () => {
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [userCircles, setUserCircles] = useState([])
|
||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||
const [circleInviteCode, setCircleInviteCode] = useState('')
|
||||
@@ -55,11 +55,6 @@ const Settings = () => {
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||
useEffect(() => {
|
||||
GetUserProfile().then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
})
|
||||
})
|
||||
GetUserCircle().then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUserCircles(data.res ? data.res : [])
|
||||
|
||||
@@ -7,15 +7,15 @@ import {
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { GetStorageUsage } from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
|
||||
const StorageSettings = () => {
|
||||
const Navigate = useNavigate()
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const [usage, setUsage] = useState({ used: 0, total: 0 })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
|
||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { ChoresGrouper } from '../../utils/Chores'
|
||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
||||
@@ -167,7 +167,8 @@ const USER_FILTER = (history, userId) => {
|
||||
}
|
||||
|
||||
const UserActivites = () => {
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [tabValue, setTabValue] = React.useState(30)
|
||||
const [selectedHistory, setSelectedHistory] = React.useState([])
|
||||
const [enrichedHistory, setEnrichedHistory] = React.useState([])
|
||||
|
||||
@@ -22,12 +22,11 @@ import {
|
||||
Tabs,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import LoadingComponent from '../components/Loading.jsx'
|
||||
|
||||
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
|
||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { RedeemPoints } from '../../utils/Fetcher.jsx'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
||||
import RedeemPointsModal from '../Modals/RedeemPointsModal'
|
||||
@@ -47,7 +46,7 @@ const UserPoints = () => {
|
||||
handleLimitChange: handleChoresHistoryLimitChange,
|
||||
} = useChoresHistory(7)
|
||||
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const [selectedUser, setSelectedUser] = useState(userProfile?.id)
|
||||
const [circleUsers, setCircleUsers] = useState([])
|
||||
const [selectedHistory, setSelectedHistory] = useState([])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Add } from '@mui/icons-material'
|
||||
import { Add, EditNotifications } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -14,15 +14,20 @@ import {
|
||||
import { FormControl } from '@mui/material'
|
||||
import * as chrono from 'chrono-node'
|
||||
import moment from 'moment'
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCreateChore } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import { parseLabels, parsePriority, parseRepeatV2 } from './CustomParsers'
|
||||
import {
|
||||
parseDueDate,
|
||||
parseLabels,
|
||||
parsePriority,
|
||||
parseRepeatV2,
|
||||
} from './CustomParsers'
|
||||
import SmartTaskTitleInput from './SmartTaskTitleInput'
|
||||
|
||||
import NotificationTemplate from '../../components/NotificationTemplate'
|
||||
import LearnMoreButton from './LearnMore'
|
||||
import RichTextEditor from './RichTextEditor'
|
||||
import SubTasks from './SubTask'
|
||||
@@ -33,7 +38,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
useCircleMembers()
|
||||
const createChoreMutation = useCreateChore()
|
||||
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [taskText, setTaskText] = useState('')
|
||||
const [taskTitle, setTaskTitle] = useState('')
|
||||
const [renderedParts, setRenderedParts] = useState([])
|
||||
@@ -46,11 +52,14 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const [assignees, setAssignees] = useState([])
|
||||
const [labelsV2, setLabelsV2] = useState([])
|
||||
const [frequency, setFrequency] = useState(null)
|
||||
const [notificationMetadata, setNotificationMetadata] = useState({
|
||||
templates: [],
|
||||
})
|
||||
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
|
||||
const [subTasks, setSubTasks] = useState(null)
|
||||
const [hasDescription, setHasDescription] = useState(false)
|
||||
const [hasSubTasks, setHasSubTasks] = useState(false)
|
||||
|
||||
const [hasNotifications, setHasNotifications] = useState(false)
|
||||
useEffect(() => {
|
||||
if (isModalOpen && textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
@@ -233,25 +242,22 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
// },
|
||||
// ])
|
||||
// }
|
||||
const parsedDueDate = chrono.parse(sentence, new Date(), {
|
||||
forwardDate: true,
|
||||
})
|
||||
if (parsedDueDate[0]?.index > -1) {
|
||||
setDueDate(
|
||||
moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'),
|
||||
)
|
||||
cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '')
|
||||
// Parse due date
|
||||
const dueDateParsed = parseDueDate(sentence, chrono)
|
||||
let dueDateHighlight = null
|
||||
if (dueDateParsed.result) {
|
||||
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
|
||||
cleanedSentence = dueDateParsed.cleanedSentence
|
||||
dueDateHighlight = dueDateParsed.highlight[0]
|
||||
}
|
||||
|
||||
if (repeat.result) {
|
||||
// if repeat has result the cleaned sentence will remove the date related info which mean
|
||||
// we need to reparse the date again to get the correct due date:
|
||||
const parsedDueDate = chrono.parse(sentence, new Date(), {
|
||||
forwardDate: true,
|
||||
})
|
||||
if (parsedDueDate[0]?.index > -1) {
|
||||
const dueDateParsedAgain = parseDueDate(sentence, chrono)
|
||||
if (dueDateParsedAgain.result) {
|
||||
setDueDate(
|
||||
moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'),
|
||||
moment(dueDateParsedAgain.result).format('YYYY-MM-DDTHH:mm:ss'),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -263,19 +269,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
repeat.highlight,
|
||||
priority.highlight,
|
||||
labels.highlight,
|
||||
parsedDueDate && parsedDueDate[0]
|
||||
? {
|
||||
start: parsedDueDate[0].index,
|
||||
end: parsedDueDate[0].index + parsedDueDate[0].text.length,
|
||||
text: parsedDueDate[0].text,
|
||||
}
|
||||
: null,
|
||||
dueDateHighlight,
|
||||
)
|
||||
|
||||
setRenderedParts(parts)
|
||||
setTaskTitle(plainText)
|
||||
},
|
||||
[circleMembers, userLabels, userProfile, renderHighlightedSentence],
|
||||
[userLabels, renderHighlightedSentence],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -313,9 +313,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
createChore()
|
||||
handleCloseModal()
|
||||
setTaskText('')
|
||||
console.log('Submitting task:', isPlusAccount(userProfile))
|
||||
|
||||
// createChore()
|
||||
// handleCloseModal()
|
||||
// setTaskText('')
|
||||
}
|
||||
|
||||
const createChore = () => {
|
||||
@@ -327,8 +329,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
assignedTo: assignees.length > 0 ? assignees[0].userId : userProfile.id,
|
||||
assignStrategy: 'random',
|
||||
isRolling: false,
|
||||
notification: false,
|
||||
description: description || null,
|
||||
|
||||
labelsV2: labelsV2,
|
||||
priority: priority ? Number(priority) : 0,
|
||||
status: 0,
|
||||
@@ -342,9 +343,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
chore.frequencyType = frequency.frequencyType
|
||||
chore.frequencyMetadata = frequency.frequencyMetadata
|
||||
chore.frequency = frequency.frequency
|
||||
if (isPlusAccount()) {
|
||||
if (isPlusAccount(userProfile)) {
|
||||
chore.notification = true
|
||||
chore.notificationMetadata = { dueDate: true }
|
||||
chore.notificationMetadata = notificationMetadata
|
||||
}
|
||||
}
|
||||
if (!frequency && dueDate) {
|
||||
@@ -518,6 +519,23 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
Due Date
|
||||
</Button>
|
||||
)}
|
||||
{!hasNotifications && dueDate && (
|
||||
<Button
|
||||
startDecorator={<EditNotifications />}
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setHasNotifications(true)
|
||||
setFrequencyHumanReadable('Once')
|
||||
setFrequency(null)
|
||||
setDueDate(
|
||||
moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'),
|
||||
)
|
||||
}}
|
||||
>
|
||||
Edit Notifications
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{hasDescription && (
|
||||
@@ -606,10 +624,29 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
)}
|
||||
</Box>
|
||||
</FormControl> */}
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Frequency</Typography>
|
||||
<Input value={frequencyHumanReadable || 'Once'} variant='plain' />
|
||||
</FormControl>
|
||||
{hasNotifications && dueDate && (
|
||||
<Box
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Notification Schedule</Typography>
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<NotificationTemplate
|
||||
onChange={metadata => {
|
||||
const newNotificaitonMetadata = {
|
||||
...notificationMetadata,
|
||||
templates: metadata.notifications,
|
||||
}
|
||||
setNotificationMetadata(newNotificaitonMetadata)
|
||||
}}
|
||||
value={notificationMetadata}
|
||||
showTimeline={false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { CalendarMonth } from '@mui/icons-material'
|
||||
import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Calendar from 'react-calendar'
|
||||
import 'react-calendar/dist/Calendar.css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { TASK_COLOR } from '../../utils/Colors'
|
||||
import './Calendar.css'
|
||||
|
||||
@@ -17,7 +16,8 @@ const getAssigneeColor = (assignee, userProfile) => {
|
||||
}
|
||||
|
||||
const CalendarView = ({ chores }) => {
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const [selectedDate, setSeletedDate] = useState(null)
|
||||
const Navigate = useNavigate()
|
||||
|
||||
|
||||
@@ -402,3 +402,80 @@ export const parseAssignees = (inputSentence, users) => {
|
||||
}
|
||||
return { result: null, cleanedSentence: sentence }
|
||||
}
|
||||
|
||||
export const parseDueDate = (inputSentence, chrono) => {
|
||||
// Parse the due date using chrono
|
||||
const parsedDueDate = chrono.parse(inputSentence, new Date(), {
|
||||
forwardDate: true,
|
||||
})
|
||||
|
||||
if (!parsedDueDate[0] || parsedDueDate[0].index === -1) {
|
||||
return {
|
||||
result: null,
|
||||
highlight: [],
|
||||
cleanedSentence: inputSentence,
|
||||
}
|
||||
}
|
||||
|
||||
const dueDateMatch = parsedDueDate[0]
|
||||
const dueDateText = dueDateMatch.text
|
||||
const dueDateStartIndex = dueDateMatch.index
|
||||
const dueDateEndIndex = dueDateStartIndex + dueDateText.length
|
||||
|
||||
// Define words that might precede the due date and should be removed
|
||||
const precedingWords = [
|
||||
'starting',
|
||||
'from',
|
||||
'beginning',
|
||||
'begin',
|
||||
'commence',
|
||||
'commencing',
|
||||
]
|
||||
|
||||
// Look for preceding words before the due date
|
||||
let cleanStartIndex = dueDateStartIndex
|
||||
let highlightStartIndex = dueDateStartIndex
|
||||
let precedingWord = ''
|
||||
|
||||
// Extract text before the due date to check for preceding words
|
||||
const textBeforeDueDate = inputSentence.substring(0, dueDateStartIndex).trim()
|
||||
|
||||
for (const word of precedingWords) {
|
||||
// Check if the text before due date ends with this preceding word
|
||||
const wordPattern = new RegExp(`\\b${word}\\s*$`, 'i')
|
||||
const match = textBeforeDueDate.match(wordPattern)
|
||||
|
||||
if (match) {
|
||||
// Found a preceding word, include it in the text to be removed
|
||||
const matchStart = textBeforeDueDate.length - match[0].length
|
||||
cleanStartIndex = matchStart
|
||||
highlightStartIndex = matchStart
|
||||
precedingWord = match[0].trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create the highlight text
|
||||
const fullHighlightText = precedingWord
|
||||
? `${precedingWord} ${dueDateText}`
|
||||
: dueDateText
|
||||
|
||||
// Create cleaned sentence by removing the full match (preceding word + due date)
|
||||
const textToRemove = inputSentence.substring(cleanStartIndex, dueDateEndIndex)
|
||||
const cleanedSentence = inputSentence
|
||||
.replace(textToRemove, '')
|
||||
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
||||
.trim()
|
||||
|
||||
return {
|
||||
result: dueDateMatch.start.date(),
|
||||
highlight: [
|
||||
{
|
||||
text: fullHighlightText,
|
||||
start: highlightStartIndex,
|
||||
end: dueDateEndIndex,
|
||||
},
|
||||
],
|
||||
cleanedSentence: cleanedSentence,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user