Merge branch 'main' into feature/internationalization-support
This commit is contained in:
@@ -20,12 +20,9 @@ As an avid for open-source, I was eager to create a solution that could benefit
|
|||||||
- Recurring Tasks: Schedule tasks to repeat daily, weekly, monthly, or yearly, with flexible customization options.
|
- Recurring Tasks: Schedule tasks to repeat daily, weekly, monthly, or yearly, with flexible customization options.
|
||||||
- Progress Tracking: Track the completion status of tasks and view historical data.
|
- Progress Tracking: Track the completion status of tasks and view historical data.
|
||||||
|
|
||||||
## Installation
|
## Development Environment
|
||||||
|
|
||||||
1. Clone the repository:
|
Follow the full instructions here: https://github.com/donetick/donetick?tab=readme-ov-file#development-environment
|
||||||
2. Navigate to the project directory: `cd frontend`
|
|
||||||
3. Download dependency `npm install`
|
|
||||||
4. Run locally `npm start`
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -41,7 +38,7 @@ Contributions are welcome! If you would like to contribute to Donetick, please f
|
|||||||
|
|
||||||
Donetick is a work in progress and has been a fantastic learning experience for me as I've honed my React skills,I'm looking for collaborators to help improve and refine the Donetick. Feel free to open PR or suggest changes.
|
Donetick is a work in progress and has been a fantastic learning experience for me as I've honed my React skills,I'm looking for collaborators to help improve and refine the Donetick. Feel free to open PR or suggest changes.
|
||||||
|
|
||||||
## Plans :
|
## Plans:
|
||||||
|
|
||||||
My goal is to expand Donetick by offering a hosted infrastructure option. This will make it even easier for users to access and utilize Donetick's features without the need for self-hosting.
|
My goal is to expand Donetick by offering a hosted infrastructure option. This will make it even easier for users to access and utilize Donetick's features without the need for self-hosting.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Input from '@mui/joy/Input'
|
|||||||
import Option from '@mui/joy/Option'
|
import Option from '@mui/joy/Option'
|
||||||
import Select from '@mui/joy/Select'
|
import Select from '@mui/joy/Select'
|
||||||
import Typography from '@mui/joy/Typography'
|
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 { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
||||||
import { TIME_UNITS } from '../utils/DurationUtils'
|
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 [error, setError] = useState(null)
|
||||||
const [showSaveDefault, setShowSaveDefault] = useState(false)
|
const [showSaveDefault, setShowSaveDefault] = useState(false)
|
||||||
// Create a map of notification indices for timeline display
|
// Create a map of notification indices for timeline display
|
||||||
@@ -114,7 +117,6 @@ const NotificationTemplate = ({
|
|||||||
// Sort notifications and update the index mapping
|
// Sort notifications and update the index mapping
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNotificationIndices()
|
updateNotificationIndices()
|
||||||
setError(null)
|
|
||||||
}, [updateNotificationIndices])
|
}, [updateNotificationIndices])
|
||||||
|
|
||||||
// Notify parent component of changes including the template name
|
// Notify parent component of changes including the template name
|
||||||
@@ -125,8 +127,8 @@ const NotificationTemplate = ({
|
|||||||
}, [notifications, onChange])
|
}, [notifications, onChange])
|
||||||
|
|
||||||
// Validates if a notification configuration already exists
|
// Validates if a notification configuration already exists
|
||||||
const isDuplicate = (notification, currentIdx = -1) => {
|
const isDuplicate = (notification, currentIdx = -1, list = notifications) => {
|
||||||
return notifications.some((n, idx) => {
|
return list.some((n, idx) => {
|
||||||
if (idx === currentIdx) return false
|
if (idx === currentIdx) return false
|
||||||
|
|
||||||
return (
|
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 handleChange = (idx, field, value) => {
|
||||||
const currentNotification = notifications[idx]
|
const currentNotification = notifications[idx]
|
||||||
const uiRep = getUIRepresentation(currentNotification)
|
const uiRep = getUIRepresentation(currentNotification)
|
||||||
@@ -149,6 +171,15 @@ const NotificationTemplate = ({
|
|||||||
// Reset display value when switching to "On Due"
|
// Reset display value when switching to "On Due"
|
||||||
if (value === 'ondue') {
|
if (value === 'ondue') {
|
||||||
updatedUIRep.displayValue = 0
|
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') {
|
} else if (field === 'displayValue') {
|
||||||
updatedUIRep.displayValue = Math.max(0, Number(value))
|
updatedUIRep.displayValue = Math.max(0, Number(value))
|
||||||
@@ -168,71 +199,41 @@ const NotificationTemplate = ({
|
|||||||
unit: updatedUIRep.unit,
|
unit: updatedUIRep.unit,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if another notification is already "On Due" (value = 0)
|
const updated = notifications.map((n, i) =>
|
||||||
if (newInternalValue === 0) {
|
i === idx ? updatedNotification : n,
|
||||||
const existingOnDue = notifications.findIndex(
|
)
|
||||||
(n, i) => i !== idx && Number(n.value) === 0,
|
setNotifications(updated)
|
||||||
)
|
notificationsRef.current = updated
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
|
||||||
if (existingOnDue !== -1) {
|
const handleBlur = idx => {
|
||||||
setError(
|
const currentList = notificationsRef.current
|
||||||
'Only one notification can be set to "On Due". Please choose a different timing.',
|
const currentNotification = currentList[idx]
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDuplicate(updatedNotification, idx)) {
|
if (!currentNotification) return
|
||||||
|
|
||||||
|
if (isDuplicate(currentNotification, idx, currentList)) {
|
||||||
setError(
|
setError(
|
||||||
'This notification setting already exists. Please use a different timing.',
|
'This notification setting already exists. Please use a different timing.',
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = notifications.map((n, i) =>
|
|
||||||
i === idx ? updatedNotification : n,
|
|
||||||
)
|
|
||||||
setNotifications(updated)
|
|
||||||
setError(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSmartNotification = type => {
|
const addSmartNotification = type => {
|
||||||
if (notifications.length >= maxNotifications) return
|
if (notifications.length >= maxNotifications) return
|
||||||
setShowSaveDefault(true)
|
setShowSaveDefault(true)
|
||||||
let newNotification
|
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) {
|
if (!newNotification) {
|
||||||
setError(`All common ${type} times are already configured.`)
|
setError(`All common ${type} times are already configured.`)
|
||||||
return
|
return
|
||||||
@@ -243,15 +244,25 @@ const NotificationTemplate = ({
|
|||||||
const updatedNotifications = [...notifications, newNotification]
|
const updatedNotifications = [...notifications, newNotification]
|
||||||
|
|
||||||
setNotifications(updatedNotifications)
|
setNotifications(updatedNotifications)
|
||||||
|
notificationsRef.current = updatedNotifications
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeNotification = idx => {
|
const removeNotification = idx => {
|
||||||
const updated = notifications.filter((_, i) => i !== idx)
|
const updated = notifications.filter((_, i) => i !== idx)
|
||||||
setNotifications(updated)
|
setNotifications(updated)
|
||||||
|
notificationsRef.current = updated
|
||||||
|
|
||||||
|
setDraftValues(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[idx]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
onChange && onChange(updated)
|
onChange && onChange(updated)
|
||||||
setShowSaveDefault(true)
|
setShowSaveDefault(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderTimeline = () => {
|
const renderTimeline = () => {
|
||||||
// Convert notifications to minutes for proper chronological sorting
|
// Convert notifications to minutes for proper chronological sorting
|
||||||
const convertToMinutes = (value, unit) => {
|
const convertToMinutes = (value, unit) => {
|
||||||
@@ -459,6 +470,11 @@ const NotificationTemplate = ({
|
|||||||
const badgeNumber = notificationIndexMap[idx]
|
const badgeNumber = notificationIndexMap[idx]
|
||||||
const uiRep = getUIRepresentation(n)
|
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 => {
|
const getNotificationColors = value => {
|
||||||
if (Number(value) < 0) {
|
if (Number(value) < 0) {
|
||||||
return {
|
return {
|
||||||
@@ -487,7 +503,7 @@ const NotificationTemplate = ({
|
|||||||
const colors = getNotificationColors(n.value)
|
const colors = getNotificationColors(n.value)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box key={idx} sx={{ position: 'relative' }}>
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={badgeNumber}
|
badgeContent={badgeNumber}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
@@ -495,7 +511,6 @@ const NotificationTemplate = ({
|
|||||||
'--Badge-minHeight': '16px',
|
'--Badge-minHeight': '16px',
|
||||||
'--Badge-fontSize': '0.7rem',
|
'--Badge-fontSize': '0.7rem',
|
||||||
'--Badge-paddingX': '5px',
|
'--Badge-paddingX': '5px',
|
||||||
|
|
||||||
top: 10,
|
top: 10,
|
||||||
'& .MuiBadge-badge': {
|
'& .MuiBadge-badge': {
|
||||||
background: colors.bgColor,
|
background: colors.bgColor,
|
||||||
@@ -504,7 +519,6 @@ const NotificationTemplate = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
key={idx}
|
|
||||||
sx={{
|
sx={{
|
||||||
mb: 1.5,
|
mb: 1.5,
|
||||||
p: 2,
|
p: 2,
|
||||||
@@ -548,11 +562,16 @@ const NotificationTemplate = ({
|
|||||||
<Select
|
<Select
|
||||||
value={uiRep.timing}
|
value={uiRep.timing}
|
||||||
onChange={(_, value) => handleChange(idx, 'timing', value)}
|
onChange={(_, value) => handleChange(idx, 'timing', value)}
|
||||||
|
onBlur={() => handleBlur(idx)}
|
||||||
sx={{ minWidth: 80 }}
|
sx={{ minWidth: 80 }}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
>
|
>
|
||||||
{timingOptions.map(opt => (
|
{timingOptions.map(opt => (
|
||||||
<Option key={opt.value} value={opt.value}>
|
<Option
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
|
||||||
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
@@ -560,11 +579,42 @@ const NotificationTemplate = ({
|
|||||||
<Input
|
<Input
|
||||||
type={'number'}
|
type={'number'}
|
||||||
min={0}
|
min={0}
|
||||||
value={uiRep.displayValue}
|
value={
|
||||||
disabled={uiRep.timing === 'ondue'}
|
draftValues[idx] !== undefined
|
||||||
onChange={e =>
|
? draftValues[idx]
|
||||||
handleChange(idx, 'displayValue', e.target.value)
|
: 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={{
|
sx={{
|
||||||
width: 60,
|
width: 60,
|
||||||
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
||||||
@@ -576,6 +626,7 @@ const NotificationTemplate = ({
|
|||||||
value={n.unit}
|
value={n.unit}
|
||||||
disabled={uiRep.timing === 'ondue'}
|
disabled={uiRep.timing === 'ondue'}
|
||||||
onChange={(_, value) => handleChange(idx, 'unit', value)}
|
onChange={(_, value) => handleChange(idx, 'unit', value)}
|
||||||
|
onBlur={() => handleBlur(idx)}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
||||||
@@ -605,7 +656,7 @@ const NotificationTemplate = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</Box>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ html {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Prevent iOS Safari from auto-zooming on input focus (triggered when font-size < 16px) */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-size: max(16px, 1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure smooth transitions for dynamic content */
|
/* Ensure smooth transitions for dynamic content */
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') =>
|
|||||||
sameElse: `MMM D ${timeFormat}`,
|
sameElse: `MMM D ${timeFormat}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// if seconds and minutes set to 59, treat as no time (date only)
|
|
||||||
if (dueDate.seconds() === 59 && dueDate.minutes() === 59) {
|
// if time is 23:59:59, treat as end-of-day (date only, no specific time)
|
||||||
|
if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) {
|
||||||
if (diff < 0) {
|
if (diff < 0) {
|
||||||
// For overdue dates, show calendar format for recent dates
|
// For overdue dates, show calendar format for recent dates
|
||||||
const absDiff = Math.abs(diff)
|
const absDiff = Math.abs(diff)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const ChoreHistoryStatus = Object.freeze({
|
|||||||
SKIPPED: 2,
|
SKIPPED: 2,
|
||||||
PENDING_APPROVAL: 3,
|
PENDING_APPROVAL: 3,
|
||||||
REJECTED: 4,
|
REJECTED: 4,
|
||||||
|
MISSED: 5,
|
||||||
|
RESCHEDULED: 6,
|
||||||
})
|
})
|
||||||
export const ChoreStatus = Object.freeze({
|
export const ChoreStatus = Object.freeze({
|
||||||
INACTIVE: 0,
|
INACTIVE: 0,
|
||||||
@@ -324,7 +326,7 @@ export const notInCompletionWindow = chore => {
|
|||||||
chore.completionWindow &&
|
chore.completionWindow &&
|
||||||
chore.completionWindow > -1 &&
|
chore.completionWindow > -1 &&
|
||||||
chore.nextDueDate &&
|
chore.nextDueDate &&
|
||||||
moment() < moment(chore.nextDueDate).add(-chore.completionWindow, 'seconds')
|
moment() < moment(chore.nextDueDate).add(-chore.completionWindow, 'hours')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export const ChoreFilters = userId => ({
|
export const ChoreFilters = userId => ({
|
||||||
@@ -332,6 +334,9 @@ export const ChoreFilters = userId => ({
|
|||||||
assigned_to_me: chore => {
|
assigned_to_me: chore => {
|
||||||
return chore.assignedTo && chore.assignedTo === userId
|
return chore.assignedTo && chore.assignedTo === userId
|
||||||
},
|
},
|
||||||
|
available_for_me: chore => {
|
||||||
|
return chore.assignedTo === null || chore.assignedTo === userId
|
||||||
|
},
|
||||||
assigned_to_others: chore => {
|
assigned_to_others: chore => {
|
||||||
return chore.assignedTo && chore.assignedTo !== userId
|
return chore.assignedTo && chore.assignedTo !== userId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -771,17 +771,19 @@ const LoginView = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
{!resource?.is_user_creation_disabled && (
|
||||||
onClick={() => {
|
<Button
|
||||||
Navigate('/signup')
|
onClick={() => {
|
||||||
}}
|
Navigate('/signup')
|
||||||
fullWidth
|
}}
|
||||||
variant='soft'
|
fullWidth
|
||||||
size='lg'
|
variant='soft'
|
||||||
// sx={{ mt: 3, mb: 2 }}
|
size='lg'
|
||||||
>
|
// sx={{ mt: 3, mb: 2 }}
|
||||||
Create new account
|
>
|
||||||
</Button>
|
Create new account
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: 'flex', justifyContent: 'center', gap: 2, mt: 2 }}
|
sx={{ display: 'flex', justifyContent: 'center', gap: 2, mt: 2 }}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { Add, HorizontalRule, Save } from '@mui/icons-material'
|
import { Add, ArrowDropDown, HorizontalRule, Save } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
|
Dropdown,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Option,
|
Option,
|
||||||
Radio,
|
Radio,
|
||||||
@@ -291,7 +296,7 @@ const ChoreEdit = () => {
|
|||||||
|
|
||||||
if (dueDateOnly) {
|
if (dueDateOnly) {
|
||||||
const combinedDateTime = moment(`${dueDateOnly}T${defaultTime}`).format(
|
const combinedDateTime = moment(`${dueDateOnly}T${defaultTime}`).format(
|
||||||
'YYYY-MM-DDTHH:mm:00',
|
'YYYY-MM-DDTHH:mm:59',
|
||||||
)
|
)
|
||||||
setDueDate(combinedDateTime)
|
setDueDate(combinedDateTime)
|
||||||
|
|
||||||
@@ -309,7 +314,7 @@ const ChoreEdit = () => {
|
|||||||
if (dueDateOnly) {
|
if (dueDateOnly) {
|
||||||
const endOfDay = moment(dueDateOnly)
|
const endOfDay = moment(dueDateOnly)
|
||||||
.endOf('day')
|
.endOf('day')
|
||||||
.format('YYYY-MM-DDTHH:mm:00')
|
.format('YYYY-MM-DDTHH:mm:ss')
|
||||||
setDueDate(endOfDay)
|
setDueDate(endOfDay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,7 +563,7 @@ const ChoreEdit = () => {
|
|||||||
const today = moment(new Date()).format('YYYY-MM-DD')
|
const today = moment(new Date()).format('YYYY-MM-DD')
|
||||||
setDueDateOnly(today)
|
setDueDateOnly(today)
|
||||||
// Default to end of day
|
// Default to end of day
|
||||||
setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'))
|
setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'))
|
||||||
setUseCustomTime(false)
|
setUseCustomTime(false)
|
||||||
setDueTime(null)
|
setDueTime(null)
|
||||||
}
|
}
|
||||||
@@ -1107,7 +1112,7 @@ const ChoreEdit = () => {
|
|||||||
const today = moment(new Date()).format('YYYY-MM-DD')
|
const today = moment(new Date()).format('YYYY-MM-DD')
|
||||||
setDueDateOnly(today)
|
setDueDateOnly(today)
|
||||||
setDueDate(
|
setDueDate(
|
||||||
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'),
|
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'),
|
||||||
)
|
)
|
||||||
setUseCustomTime(false)
|
setUseCustomTime(false)
|
||||||
setDueTime(null)
|
setDueTime(null)
|
||||||
@@ -1189,7 +1194,7 @@ const ChoreEdit = () => {
|
|||||||
checked={completionWindow !== -1}
|
checked={completionWindow !== -1}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
setCompletionWindow(3600) // default 1 hour in seconds
|
setCompletionWindow(1) // default 1 hour in seconds
|
||||||
} else {
|
} else {
|
||||||
setCompletionWindow(-1)
|
setCompletionWindow(-1)
|
||||||
}
|
}
|
||||||
@@ -1203,29 +1208,38 @@ const ChoreEdit = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{completionWindow !== -1 && (
|
{completionWindow !== -1 && (
|
||||||
<Box
|
<Card variant='outlined'>
|
||||||
sx={{
|
<Box
|
||||||
mt: 1,
|
sx={{
|
||||||
ml: 4,
|
mt: 0,
|
||||||
display: 'flex',
|
ml: 4,
|
||||||
gap: 1,
|
}}
|
||||||
alignItems: 'center',
|
>
|
||||||
}}
|
<Typography level='body-sm'>Hours:</Typography>
|
||||||
>
|
<Input
|
||||||
<DurationInput
|
type='number'
|
||||||
value={completionWindow}
|
value={completionWindow}
|
||||||
onChange={setCompletionWindow}
|
sx={{ maxWidth: 100 }}
|
||||||
size='sm'
|
slotProps={{
|
||||||
minValue={0}
|
input: {
|
||||||
/>
|
min: 0,
|
||||||
<Typography level='body-sm'>before due date</Typography>
|
max: 24 * 7,
|
||||||
</Box>
|
},
|
||||||
|
}}
|
||||||
|
placeholder='Hours'
|
||||||
|
onChange={e => {
|
||||||
|
setCompletionWindow(parseInt(e.target.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Expires After (Deadline) */}
|
{/* Expires After (Deadline) */}
|
||||||
<FormControl sx={{ mt: 2 }}>
|
{/* <FormControl sx={{ mt: 2 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={deadlineOffset !== -1}
|
checked={deadlineOffset !== -1}
|
||||||
|
disabled={isRolling}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
setDeadlineOffset(86400) // default 1 day in seconds
|
setDeadlineOffset(86400) // default 1 day in seconds
|
||||||
@@ -1237,9 +1251,11 @@ const ChoreEdit = () => {
|
|||||||
label='Set a deadline'
|
label='Set a deadline'
|
||||||
/>
|
/>
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
Task will be considered expired after the due date
|
{isRolling && !['once', 'no_repeat'].includes(frequencyType)
|
||||||
|
? 'Deadline is not available when scheduling from completion date'
|
||||||
|
: 'Task will be considered expired after the due date'}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl> */}
|
||||||
|
|
||||||
{deadlineOffset !== -1 && (
|
{deadlineOffset !== -1 && (
|
||||||
<Box
|
<Box
|
||||||
@@ -1286,7 +1302,10 @@ const ChoreEdit = () => {
|
|||||||
<Radio
|
<Radio
|
||||||
overlay
|
overlay
|
||||||
checked={isRolling}
|
checked={isRolling}
|
||||||
onClick={() => setIsRolling(true)}
|
onClick={() => {
|
||||||
|
setIsRolling(true)
|
||||||
|
setDeadlineOffset(-1)
|
||||||
|
}}
|
||||||
label='Reschedule from completion date'
|
label='Reschedule from completion date'
|
||||||
/>
|
/>
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
@@ -1624,39 +1643,38 @@ const ChoreEdit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{choreId > 0 && (
|
{choreId > 0 && (
|
||||||
<>
|
<Dropdown>
|
||||||
{isActive ? (
|
<ButtonGroup
|
||||||
<Button
|
variant='outlined'
|
||||||
color='danger'
|
color={isActive ? 'danger' : 'neutral'}
|
||||||
variant='outlined'
|
|
||||||
onClick={() => {
|
|
||||||
archiveChore.mutate(choreId)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Archive
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
color='neutral'
|
|
||||||
variant='outlined'
|
|
||||||
onClick={() => {
|
|
||||||
unarchiveChore.mutate(choreId)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unarchive
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
color='danger'
|
|
||||||
variant='solid'
|
|
||||||
onClick={() => {
|
|
||||||
// confirm before deleting:
|
|
||||||
handleDelete()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Delete
|
<Button
|
||||||
</Button>
|
onClick={() => {
|
||||||
</>
|
isActive
|
||||||
|
? archiveChore.mutate(choreId)
|
||||||
|
: unarchiveChore.mutate(choreId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isActive ? 'Archive' : 'Unarchive'}
|
||||||
|
</Button>
|
||||||
|
<MenuButton
|
||||||
|
slots={{ root: IconButton }}
|
||||||
|
slotProps={{
|
||||||
|
root: {
|
||||||
|
variant: 'outlined',
|
||||||
|
color: isActive ? 'danger' : 'neutral',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowDropDown />
|
||||||
|
</MenuButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Menu placement='top-end'>
|
||||||
|
<MenuItem color='danger' onClick={handleDelete}>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color='neutral'
|
color='neutral'
|
||||||
|
|||||||
@@ -673,7 +673,6 @@ const ChoreView = () => {
|
|||||||
color='neutral'
|
color='neutral'
|
||||||
variant='plain'
|
variant='plain'
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={chore.isActive === false}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/chores/${choreId}/history`)
|
navigate(`/chores/${choreId}/history`)
|
||||||
}}
|
}}
|
||||||
@@ -692,7 +691,6 @@ const ChoreView = () => {
|
|||||||
color='neutral'
|
color='neutral'
|
||||||
variant='plain'
|
variant='plain'
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={chore.isActive === false}
|
|
||||||
sx={{
|
sx={{
|
||||||
// top right of the card:
|
// top right of the card:
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -1018,9 +1016,7 @@ const ChoreView = () => {
|
|||||||
size='lg'
|
size='lg'
|
||||||
onClick={handleTaskCompletion}
|
onClick={handleTaskCompletion}
|
||||||
disabled={
|
disabled={
|
||||||
notInCompletionWindow(chore) ||
|
notInCompletionWindow(chore) || chore.isActive === false
|
||||||
(chore.lastCompletedDate !== null &&
|
|
||||||
chore.frequencyType === 'once')
|
|
||||||
}
|
}
|
||||||
color='success'
|
color='success'
|
||||||
startDecorator={<Check />}
|
startDecorator={<Check />}
|
||||||
@@ -1050,9 +1046,7 @@ const ChoreView = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
notInCompletionWindow(chore) ||
|
notInCompletionWindow(chore) || chore.isActive === false
|
||||||
(chore.lastCompletedDate !== null &&
|
|
||||||
chore.frequencyType === 'once')
|
|
||||||
}
|
}
|
||||||
startDecorator={<SwitchAccessShortcut />}
|
startDecorator={<SwitchAccessShortcut />}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -1071,7 +1065,7 @@ const ChoreView = () => {
|
|||||||
>
|
>
|
||||||
Available to complete starting{' '}
|
Available to complete starting{' '}
|
||||||
{moment(chore.nextDueDate)
|
{moment(chore.nextDueDate)
|
||||||
.subtract(chore.completionWindow, 'seconds')
|
.subtract(chore.completionWindow, 'hours')
|
||||||
.format('MM/DD/YYYY hh:mm A')}
|
.format('MM/DD/YYYY hh:mm A')}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -1081,8 +1075,7 @@ const ChoreView = () => {
|
|||||||
disabled={
|
disabled={
|
||||||
(chore.status === ChoreStatus.PAUSED &&
|
(chore.status === ChoreStatus.PAUSED &&
|
||||||
notInCompletionWindow(chore)) ||
|
notInCompletionWindow(chore)) ||
|
||||||
(chore.lastCompletedDate !== null &&
|
chore.isActive === false
|
||||||
chore.frequencyType === 'once')
|
|
||||||
}
|
}
|
||||||
chore={chore}
|
chore={chore}
|
||||||
onAction={action => {
|
onAction={action => {
|
||||||
@@ -1108,9 +1101,7 @@ const ChoreView = () => {
|
|||||||
variant='soft'
|
variant='soft'
|
||||||
color='success'
|
color='success'
|
||||||
disabled={
|
disabled={
|
||||||
notInCompletionWindow(chore) ||
|
notInCompletionWindow(chore) || chore.isActive === false
|
||||||
(chore.lastCompletedDate !== null &&
|
|
||||||
chore.frequencyType === 'once')
|
|
||||||
}
|
}
|
||||||
startDecorator={<PlayArrow />}
|
startDecorator={<PlayArrow />}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
|
|||||||
icon: <Timelapse />,
|
icon: <Timelapse />,
|
||||||
}
|
}
|
||||||
} else if (activity.status === 1) {
|
} else if (activity.status === 1) {
|
||||||
const wasOnTime = moment(activity.performedAt).isSameOrBefore(
|
const wasOnTime =
|
||||||
moment(activity.dueDate),
|
!activity.dueDate ||
|
||||||
)
|
moment(activity.performedAt).isSameOrBefore(moment(activity.dueDate))
|
||||||
|
|
||||||
if (wasOnTime) {
|
if (wasOnTime) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const ChoreCard = ({
|
|||||||
const getName = name => {
|
const getName = name => {
|
||||||
const split = Array.from(chore.name)
|
const split = Array.from(chore.name)
|
||||||
// if the first character is emoji then remove it from the name
|
// if the first character is emoji then remove it from the name
|
||||||
if (/\p{Emoji}/u.test(split[0])) {
|
if (isNaN(Number(split[0])) && /\p{Emoji}/u.test(split[0])) {
|
||||||
return split.slice(1).join('').trim()
|
return split.slice(1).join('').trim()
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|||||||
@@ -221,15 +221,12 @@ const MyChores = () => {
|
|||||||
let choresToGroup = chores
|
let choresToGroup = chores
|
||||||
if (tempFilter || activeFilterId) {
|
if (tempFilter || activeFilterId) {
|
||||||
choresToGroup = customFilteredChores
|
choresToGroup = customFilteredChores
|
||||||
} else if (selectedProject) {
|
} else if (!selectedProject || selectedProject.id === 'default') {
|
||||||
// Otherwise, use project-filtered chores for section grouping
|
// No project selected or default project: only show tasks without a projectId
|
||||||
if (selectedProject.id === 'default') {
|
choresToGroup = chores.filter(chore => !chore.projectId)
|
||||||
// Default project: only show tasks without a projectId
|
} else {
|
||||||
choresToGroup = chores.filter(chore => !chore.projectId)
|
// Specific project: use the existing filter function
|
||||||
} else {
|
choresToGroup = filterByProject(chores, selectedProject.id)
|
||||||
// Other projects: use the existing filter function
|
|
||||||
choresToGroup = filterByProject(chores, selectedProject.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = ChoresGrouper(
|
const sections = ChoresGrouper(
|
||||||
@@ -529,10 +526,11 @@ const MyChores = () => {
|
|||||||
onEnableMultiSelectAndSelectAll: () => {
|
onEnableMultiSelectAndSelectAll: () => {
|
||||||
toggleMultiSelectMode()
|
toggleMultiSelectMode()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
selectAllVisibleChores()
|
selectAllVisibleChores(null, choreSections, openChoreSections)
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
onSelectAll: () => selectAllVisibleChores(),
|
onSelectAll: () =>
|
||||||
|
selectAllVisibleChores(null, choreSections, openChoreSections),
|
||||||
onClearSelection: clearSelection,
|
onClearSelection: clearSelection,
|
||||||
onBulkComplete: handleBulkComplete,
|
onBulkComplete: handleBulkComplete,
|
||||||
onBulkSkip: handleBulkSkip,
|
onBulkSkip: handleBulkSkip,
|
||||||
@@ -1276,6 +1274,9 @@ const MyChores = () => {
|
|||||||
setSearchFilter('All')
|
setSearchFilter('All')
|
||||||
setSearchTerm('')
|
setSearchTerm('')
|
||||||
clearActiveFilter()
|
clearActiveFilter()
|
||||||
|
// reset project and filters :
|
||||||
|
setSelectedProjectWithCache(null)
|
||||||
|
updateFilterUrl(null, null)
|
||||||
}}
|
}}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ const SmartInsightsCard = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={insight.id}
|
key={insight.id}
|
||||||
variant={isActive ? 'solid' : 'soft'}
|
variant={isActive ? 'outlined' : 'soft'}
|
||||||
color='neutral'
|
color='neutral'
|
||||||
onClick={() => handleInsightClick(insight)}
|
onClick={() => handleInsightClick(insight)}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -85,7 +85,12 @@ const SortAndGrouping = ({
|
|||||||
{ name: 'Labels', value: 'labels' },
|
{ name: 'Labels', value: 'labels' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const filterItems = ['anyone', 'assigned_to_me', 'assigned_to_others']
|
const filterItems = [
|
||||||
|
'anyone',
|
||||||
|
'assigned_to_me',
|
||||||
|
'available_for_me',
|
||||||
|
'assigned_to_others',
|
||||||
|
]
|
||||||
|
|
||||||
// Total selectable items: 4 (group by) + 3 (filters) + 1 (create custom filter) = 8
|
// Total selectable items: 4 (group by) + 3 (filters) + 1 (create custom filter) = 8
|
||||||
const totalItems = groupByItems.length + filterItems.length + 1
|
const totalItems = groupByItems.length + filterItems.length + 1
|
||||||
@@ -169,6 +174,65 @@ const SortAndGrouping = ({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const MenuItem_QuickFilter = props => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={props.key}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter(props.filterKey)
|
||||||
|
handleMenuClose()
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'var(--joy-radius-sm)',
|
||||||
|
backgroundColor:
|
||||||
|
selectedFilter === props.filterKey
|
||||||
|
? 'var(--joy-palette-primary-softBg)'
|
||||||
|
: selectedIndex === props.index &&
|
||||||
|
anchorEl &&
|
||||||
|
isKeyboardNavigating
|
||||||
|
? 'var(--joy-palette-neutral-softHoverBg)'
|
||||||
|
: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
selectedFilter === props.filterKey
|
||||||
|
? 'var(--joy-palette-primary-softBg)'
|
||||||
|
: 'var(--joy-palette-neutral-softHoverBg)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<Radio
|
||||||
|
checked={selectedFilter === props.filterKey}
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
|
</ListItemDecorator>
|
||||||
|
<ListItemContent>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
sx={{
|
||||||
|
fontWeight: selectedFilter === props.filterKey ? 600 : 400,
|
||||||
|
color:
|
||||||
|
selectedFilter === props.filterKey
|
||||||
|
? 'var(--joy-palette-primary-600)'
|
||||||
|
: 'var(--joy-palette-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ListItemContent>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!label && (
|
{!label && (
|
||||||
@@ -359,162 +423,33 @@ const SortAndGrouping = ({
|
|||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-anyone`}
|
key={`${k}-assignee-anyone`}
|
||||||
onClick={() => {
|
index={4}
|
||||||
setFilter('anyone')
|
filterKey='anyone'
|
||||||
handleMenuClose()
|
label='Anyone'
|
||||||
}}
|
/>
|
||||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
|
||||||
sx={{
|
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'anyone'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: selectedIndex === 4 && anchorEl && isKeyboardNavigating
|
|
||||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
|
||||||
: 'transparent',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'anyone'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemDecorator>
|
|
||||||
<Radio checked={selectedFilter === 'anyone'} variant='outlined' />
|
|
||||||
</ListItemDecorator>
|
|
||||||
<ListItemContent>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
level='body-sm'
|
|
||||||
sx={{
|
|
||||||
fontWeight: selectedFilter === 'anyone' ? 600 : 400,
|
|
||||||
color:
|
|
||||||
selectedFilter === 'anyone'
|
|
||||||
? 'var(--joy-palette-primary-600)'
|
|
||||||
: 'var(--joy-palette-text-primary)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Anyone
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</ListItemContent>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-assigned-to-me`}
|
key={`${k}-assignee-assigned-to-me`}
|
||||||
onClick={() => {
|
index={5}
|
||||||
setFilter('assigned_to_me')
|
filterKey='assigned_to_me'
|
||||||
handleMenuClose()
|
label='Assigned to me'
|
||||||
}}
|
/>
|
||||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
|
||||||
sx={{
|
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'assigned_to_me'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: selectedIndex === 5 && anchorEl && isKeyboardNavigating
|
|
||||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
|
||||||
: 'transparent',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'assigned_to_me'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemDecorator>
|
|
||||||
<Radio
|
|
||||||
checked={selectedFilter === 'assigned_to_me'}
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
|
||||||
</ListItemDecorator>
|
|
||||||
<ListItemContent>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
level='body-sm'
|
|
||||||
sx={{
|
|
||||||
fontWeight: selectedFilter === 'assigned_to_me' ? 600 : 400,
|
|
||||||
color:
|
|
||||||
selectedFilter === 'assigned_to_me'
|
|
||||||
? 'var(--joy-palette-primary-600)'
|
|
||||||
: 'var(--joy-palette-text-primary)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Assigned to me
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</ListItemContent>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem_QuickFilter
|
||||||
|
key={`${k}-assignee-available-for-me`}
|
||||||
|
index={6}
|
||||||
|
filterKey='available_for_me'
|
||||||
|
label='Available for me'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-assigned-to-others`}
|
key={`${k}-assignee-assigned-to-others`}
|
||||||
onClick={() => {
|
index={7}
|
||||||
setFilter('assigned_to_others')
|
filterKey='assigned_to_others'
|
||||||
handleMenuClose()
|
label='Assigned to others'
|
||||||
}}
|
/>
|
||||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
|
||||||
sx={{
|
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'assigned_to_others'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: selectedIndex === 6 && anchorEl && isKeyboardNavigating
|
|
||||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
|
||||||
: 'transparent',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor:
|
|
||||||
selectedFilter === 'assigned_to_others'
|
|
||||||
? 'var(--joy-palette-primary-softBg)'
|
|
||||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemDecorator>
|
|
||||||
<Radio
|
|
||||||
checked={selectedFilter === 'assigned_to_others'}
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
|
||||||
</ListItemDecorator>
|
|
||||||
<ListItemContent>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
level='body-sm'
|
|
||||||
sx={{
|
|
||||||
fontWeight:
|
|
||||||
selectedFilter === 'assigned_to_others' ? 600 : 400,
|
|
||||||
color:
|
|
||||||
selectedFilter === 'assigned_to_others'
|
|
||||||
? 'var(--joy-palette-primary-600)'
|
|
||||||
: 'var(--joy-palette-text-primary)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Assigned to others
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</ListItemContent>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 1 }} />
|
<Divider sx={{ my: 1 }} />
|
||||||
|
|
||||||
@@ -528,7 +463,7 @@ const SortAndGrouping = ({
|
|||||||
sx={{
|
sx={{
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
borderRadius: 'var(--joy-radius-sm)',
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
selectedIndex === 7 && anchorEl && isKeyboardNavigating
|
selectedIndex === 8 && anchorEl && isKeyboardNavigating
|
||||||
? 'var(--joy-palette-success-softHoverBg)'
|
? 'var(--joy-palette-success-softHoverBg)'
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
|
|||||||
@@ -154,6 +154,18 @@ export const useChoreActions = ({
|
|||||||
async (action, chore, extraData = {}) => {
|
async (action, chore, extraData = {}) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'complete':
|
case 'complete':
|
||||||
|
// 1. Instantly hide the chore from the UI and Cache
|
||||||
|
setChores(prev => prev.filter(c => c.id !== chore.id))
|
||||||
|
setFilteredChores(prev => prev.filter(c => c.id !== chore.id))
|
||||||
|
|
||||||
|
queryClient.setQueriesData({ queryKey: ['chores'] }, oldData => {
|
||||||
|
if (!oldData || !oldData.res) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
res: oldData.res.filter(c => c.id !== chore.id),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await MarkChoreComplete(
|
const response = await MarkChoreComplete(
|
||||||
chore.id,
|
chore.id,
|
||||||
@@ -162,10 +174,36 @@ export const useChoreActions = ({
|
|||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
// 2. Show the success notification with Undo
|
||||||
updateChoreInState(data.res, 'completed')
|
showSuccess({
|
||||||
|
message: 'Task completed',
|
||||||
|
undoAction: async () => {
|
||||||
|
try {
|
||||||
|
const undoResponse = await UndoChoreAction(chore.id)
|
||||||
|
if (undoResponse.ok) {
|
||||||
|
refetchChores()
|
||||||
|
showUndo({
|
||||||
|
title: 'Undo Successful',
|
||||||
|
message: 'Task completion has been undone.',
|
||||||
|
})
|
||||||
|
} else throw new Error('Failed to undo')
|
||||||
|
} catch (error) {
|
||||||
|
showError({
|
||||||
|
title: 'Undo Failed',
|
||||||
|
message: 'Unable to undo the action. Please try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Fetch the fresh active list from the server silently
|
||||||
|
// (This brings in the next occurrence if recurring, without showing the completed one)
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['chores'] })
|
||||||
|
} else {
|
||||||
|
refetchChores() // Network failed, revert to truth
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
refetchChores() // Network failed, revert to truth
|
||||||
if (error?.queued) {
|
if (error?.queued) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Update Failed',
|
title: 'Update Failed',
|
||||||
@@ -259,6 +297,7 @@ export const useChoreActions = ({
|
|||||||
c => c.id !== chore.id,
|
c => c.id !== chore.id,
|
||||||
)
|
)
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
|
updateChoreInState(chore.id, 'deleted')
|
||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Task Deleted',
|
title: 'Task Deleted',
|
||||||
@@ -472,7 +511,7 @@ export const useChoreActions = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleBulkComplete = useCallback(async () => {
|
const handleBulkComplete = useCallback(async () => {
|
||||||
const selectedData = getSelectedChoresData()
|
const selectedData = getSelectedChoresData(chores)
|
||||||
if (selectedData.length === 0) return
|
if (selectedData.length === 0) return
|
||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
@@ -532,7 +571,7 @@ export const useChoreActions = ({
|
|||||||
}, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
}, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||||
|
|
||||||
const handleBulkArchive = useCallback(async () => {
|
const handleBulkArchive = useCallback(async () => {
|
||||||
const selectedData = getSelectedChoresData()
|
const selectedData = getSelectedChoresData(chores)
|
||||||
if (selectedData.length === 0) return
|
if (selectedData.length === 0) return
|
||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
@@ -594,7 +633,7 @@ export const useChoreActions = ({
|
|||||||
}, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
}, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||||
|
|
||||||
const handleBulkDelete = useCallback(async () => {
|
const handleBulkDelete = useCallback(async () => {
|
||||||
const selectedData = getSelectedChoresData()
|
const selectedData = getSelectedChoresData(chores)
|
||||||
if (selectedData.length === 0) return
|
if (selectedData.length === 0) return
|
||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
@@ -654,7 +693,7 @@ export const useChoreActions = ({
|
|||||||
}, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
}, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||||
|
|
||||||
const handleBulkSkip = useCallback(async () => {
|
const handleBulkSkip = useCallback(async () => {
|
||||||
const selectedData = getSelectedChoresData()
|
const selectedData = getSelectedChoresData(chores)
|
||||||
if (selectedData.length === 0) return
|
if (selectedData.length === 0) return
|
||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ export const useChoreFilters = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const projectFilteredChores = useMemo(() => {
|
const projectFilteredChores = useMemo(() => {
|
||||||
if (!selectedProject) return chores
|
if (!selectedProject || selectedProject.id === 'default') {
|
||||||
|
|
||||||
if (selectedProject.id === 'default') {
|
|
||||||
return chores.filter(chore => !chore.projectId)
|
return chores.filter(chore => !chore.projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const ChoreHistory = () => {
|
|||||||
{
|
{
|
||||||
icon: <Checklist />,
|
icon: <Checklist />,
|
||||||
text: 'All Completed',
|
text: 'All Completed',
|
||||||
subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED).length} times`,
|
subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED || h.status === ChoreHistoryStatus.SKIPPED).length} times`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TrendingUp />,
|
icon: <TrendingUp />,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Person,
|
Person,
|
||||||
Redo,
|
Redo,
|
||||||
RunningWithErrors,
|
RunningWithErrors,
|
||||||
|
Schedule,
|
||||||
ThumbDown,
|
ThumbDown,
|
||||||
Timelapse,
|
Timelapse,
|
||||||
Toll,
|
Toll,
|
||||||
@@ -18,7 +19,7 @@ import { useLocalization } from '../../contexts/LocalizationContext'
|
|||||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||||
|
|
||||||
const getCompletedChip = historyEntry => {
|
const getCompletedChip = historyEntry => {
|
||||||
if (historyEntry.status === 0 || historyEntry.status === 5) {
|
if (historyEntry.status === 0 || historyEntry.status === 5 || historyEntry.status === 6) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ const HistoryCard = ({
|
|||||||
3: { icon: <HourglassEmpty />, color: 'neutral' }, // Pending Approval
|
3: { icon: <HourglassEmpty />, color: 'neutral' }, // Pending Approval
|
||||||
4: { icon: <ThumbDown />, color: 'danger' }, // Rejected
|
4: { icon: <ThumbDown />, color: 'danger' }, // Rejected
|
||||||
5: { icon: <RunningWithErrors />, color: 'danger' }, // Missed
|
5: { icon: <RunningWithErrors />, color: 'danger' }, // Missed
|
||||||
|
6: { icon: <Schedule />, color: 'warning' }, // Rescheduled
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = statusMap[historyEntry.status] || statusMap[1]
|
const config = statusMap[historyEntry.status] || statusMap[1]
|
||||||
@@ -194,7 +196,9 @@ const HistoryCard = ({
|
|||||||
? 'Rejected'
|
? 'Rejected'
|
||||||
: historyEntry.status === 5
|
: historyEntry.status === 5
|
||||||
? 'Missed'
|
? 'Missed'
|
||||||
: 'Completed'}
|
: historyEntry.status === 6
|
||||||
|
? 'Rescheduled'
|
||||||
|
: 'Completed'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Chip size='sm' startDecorator={<EventNote />}>
|
<Chip size='sm' startDecorator={<EventNote />}>
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ const NotificationSetting = () => {
|
|||||||
<Typography level='h3'>Custom Notification</Typography>
|
<Typography level='h3'>Custom Notification</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
Notificaiton through other platform like Telegram or Pushover
|
Notification through other platform like Telegram or Pushover
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControl orientation='horizontal'>
|
<FormControl orientation='horizontal'>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
||||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'
|
|
||||||
import ThumbDownIcon from '@mui/icons-material/ThumbDown'
|
|
||||||
import TimelapseIcon from '@mui/icons-material/Timelapse'
|
|
||||||
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
|
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Block,
|
AccessTime,
|
||||||
Check,
|
Check,
|
||||||
EventBusy,
|
EventBusy,
|
||||||
|
EventNote,
|
||||||
Group,
|
Group,
|
||||||
|
HourglassEmpty,
|
||||||
|
Redo,
|
||||||
|
RunningWithErrors,
|
||||||
|
Schedule,
|
||||||
|
ThumbDown,
|
||||||
Timeline,
|
Timeline,
|
||||||
Toll,
|
Toll,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
@@ -34,6 +36,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||||
|
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
|
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
|
||||||
@@ -44,7 +47,9 @@ const groupByDate = history => {
|
|||||||
const aggregated = {}
|
const aggregated = {}
|
||||||
for (let i = 0; i < history.length; i++) {
|
for (let i = 0; i < history.length; i++) {
|
||||||
const item = history[i]
|
const item = history[i]
|
||||||
const date = new Date(item.performedAt).toLocaleDateString()
|
const date = new Date(
|
||||||
|
item.performedAt || item.updatedAt,
|
||||||
|
).toLocaleDateString()
|
||||||
if (!aggregated[date]) {
|
if (!aggregated[date]) {
|
||||||
aggregated[date] = []
|
aggregated[date] = []
|
||||||
}
|
}
|
||||||
@@ -53,21 +58,25 @@ const groupByDate = history => {
|
|||||||
return aggregated
|
return aggregated
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
|
const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onViewNote }) => {
|
||||||
const getStatusIcon = status => {
|
const getStatusIcon = status => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0:
|
case 0:
|
||||||
return <TimelapseIcon color='primary' />
|
return <AccessTime color='primary' />
|
||||||
case 1:
|
case 1:
|
||||||
return <Check color='success' />
|
return <Check color='success' />
|
||||||
case 2:
|
case 2:
|
||||||
return <Block color='warning' />
|
return <Redo color='warning' />
|
||||||
case 3:
|
case 3:
|
||||||
return <HourglassEmptyIcon color='action' />
|
return <HourglassEmpty color='neutral' />
|
||||||
case 4:
|
case 4:
|
||||||
return <ThumbDownIcon color='error' />
|
return <ThumbDown color='error' />
|
||||||
|
case 5:
|
||||||
|
return <RunningWithErrors color='error' />
|
||||||
|
case 6:
|
||||||
|
return <Schedule color='warning' />
|
||||||
default:
|
default:
|
||||||
return <CheckCircleIcon color='success' />
|
return <Check color='success' />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +123,36 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
|
|||||||
{`${points} points`}
|
{`${points} points`}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
|
{notes && (
|
||||||
|
<Chip
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
color='neutral'
|
||||||
|
startDecorator={<EventNote />}
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onViewNote?.(notes)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Note
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChoreHistoryTimeline = ({ history }) => {
|
|
||||||
|
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||||
const { fmt } = useLocalization()
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const groupedHistory = groupByDate(history)
|
const groupedHistory = groupByDate(history)
|
||||||
|
|
||||||
|
const sortedEntries = Object.entries(groupedHistory).sort(
|
||||||
|
([a], [b]) => new Date(b) - new Date(a),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container sx={{ p: 2 }}>
|
<Container sx={{ p: 2 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||||
@@ -143,10 +173,15 @@ const ChoreHistoryTimeline = ({ history }) => {
|
|||||||
<>
|
<>
|
||||||
<ChoreHistoryItem
|
<ChoreHistoryItem
|
||||||
key={record.id}
|
key={record.id}
|
||||||
time={fmt.time(record.performedAt)}
|
|
||||||
|
time={fmt.time(
|
||||||
|
record.performedAt || record.updatedAt,
|
||||||
|
)}
|
||||||
name={record.choreName}
|
name={record.choreName}
|
||||||
points={record.points}
|
points={record.points}
|
||||||
status={record.status}
|
status={record.status}
|
||||||
|
notes={record.notes}
|
||||||
|
onViewNote={onViewNote}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
@@ -366,6 +401,7 @@ const UserActivites = () => {
|
|||||||
const [selectedHistory, setSelectedHistory] = React.useState([])
|
const [selectedHistory, setSelectedHistory] = React.useState([])
|
||||||
const [enrichedHistory, setEnrichedHistory] = React.useState([])
|
const [enrichedHistory, setEnrichedHistory] = React.useState([])
|
||||||
const [selectedChart, setSelectedChart] = React.useState('history')
|
const [selectedChart, setSelectedChart] = React.useState('history')
|
||||||
|
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||||
|
|
||||||
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
|
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
|
||||||
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
|
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
|
||||||
@@ -1066,7 +1102,17 @@ const UserActivites = () => {
|
|||||||
>
|
>
|
||||||
{/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */}
|
{/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */}
|
||||||
<Box sx={{ flex: 1, minWidth: 0, width: '100%' }}>
|
<Box sx={{ flex: 1, minWidth: 0, width: '100%' }}>
|
||||||
<ChoreHistoryTimeline history={selectedHistory} />
|
<ChoreHistoryTimeline
|
||||||
|
history={selectedHistory}
|
||||||
|
onViewNote={notes => {
|
||||||
|
setNoteViewerConfig({
|
||||||
|
isOpen: true,
|
||||||
|
title: 'Note',
|
||||||
|
content: notes,
|
||||||
|
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
|
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
|
||||||
@@ -1216,6 +1262,7 @@ const UserActivites = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<NoteViewerModal config={noteViewerConfig} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { Add, EditNotifications } from '@mui/icons-material'
|
import { Add, EditNotifications } from '@mui/icons-material'
|
||||||
import { Box, Button, Input, Option, Select, Typography } from '@mui/joy'
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormHelperText,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
import { FormControl } from '@mui/material'
|
import { FormControl } from '@mui/material'
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@@ -20,7 +29,6 @@ import {
|
|||||||
} from './CustomParsers'
|
} from './CustomParsers'
|
||||||
import SmartTaskTitleInput from './SmartTaskTitleInput'
|
import SmartTaskTitleInput from './SmartTaskTitleInput'
|
||||||
|
|
||||||
import DurationInput from '../../components/common/DurationInput'
|
|
||||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||||
import NotificationTemplate from '../../components/NotificationTemplate'
|
import NotificationTemplate from '../../components/NotificationTemplate'
|
||||||
import LearnMoreButton from './LearnMore'
|
import LearnMoreButton from './LearnMore'
|
||||||
@@ -93,6 +101,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
const [hasNotifications, setHasNotifications] = useState(false)
|
const [hasNotifications, setHasNotifications] = useState(false)
|
||||||
const [hasDeadline, setHasDeadline] = useState(false)
|
const [hasDeadline, setHasDeadline] = useState(false)
|
||||||
const [deadlineOffset, setDeadlineOffset] = useState(-1)
|
const [deadlineOffset, setDeadlineOffset] = useState(-1)
|
||||||
|
const [dueDateOnly, setDueDateOnly] = useState(null)
|
||||||
|
const [dueTime, setDueTime] = useState(null)
|
||||||
|
const [useCustomTime, setUseCustomTime] = useState(false)
|
||||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
const [projectId, setProjectId] = useState(getInitialProject())
|
const [projectId, setProjectId] = useState(getInitialProject())
|
||||||
|
|
||||||
@@ -136,7 +147,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
!dueDate
|
!dueDate
|
||||||
) {
|
) {
|
||||||
// add due date:
|
// add due date:
|
||||||
setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'))
|
const tomorrow = moment().add(1, 'day')
|
||||||
|
setDueDateOnly(tomorrow.format('YYYY-MM-DD'))
|
||||||
|
setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:59'))
|
||||||
|
setUseCustomTime(false)
|
||||||
|
setDueTime(null)
|
||||||
setShowKeyboardShortcuts(false)
|
setShowKeyboardShortcuts(false)
|
||||||
}
|
}
|
||||||
// Enter key to create task
|
// Enter key to create task
|
||||||
@@ -380,9 +395,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
setFrequencyHumanReadable(repeat.name)
|
setFrequencyHumanReadable(repeat.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncDueDateStates = parsedDate => {
|
||||||
|
const m = moment(parsedDate)
|
||||||
|
const dateOnly = m.format('YYYY-MM-DD')
|
||||||
|
const timeOnly = m.format('HH:mm')
|
||||||
|
setDueDateOnly(dateOnly)
|
||||||
|
setDueDate(m.format('YYYY-MM-DDTHH:mm:ss'))
|
||||||
|
if (timeOnly !== '23:59') {
|
||||||
|
setUseCustomTime(true)
|
||||||
|
setDueTime(timeOnly)
|
||||||
|
} else {
|
||||||
|
setUseCustomTime(false)
|
||||||
|
setDueTime(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let dueDateHighlight = null
|
let dueDateHighlight = null
|
||||||
if (dueDateParsed.result) {
|
if (dueDateParsed.result) {
|
||||||
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
|
syncDueDateStates(dueDateParsed.result)
|
||||||
dueDateHighlight = dueDateParsed.highlight[0]
|
dueDateHighlight = dueDateParsed.highlight[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,9 +421,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
// we need to reparse the date again to get the correct due date:
|
// we need to reparse the date again to get the correct due date:
|
||||||
const dueDateParsedAgain = parseDueDate(sentence, chrono)
|
const dueDateParsedAgain = parseDueDate(sentence, chrono)
|
||||||
if (dueDateParsedAgain.result) {
|
if (dueDateParsedAgain.result) {
|
||||||
setDueDate(
|
syncDueDateStates(dueDateParsedAgain.result)
|
||||||
moment(dueDateParsedAgain.result).format('YYYY-MM-DDTHH:mm:ss'),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +501,47 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
processText,
|
processText,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const handleDueDateChange = e => {
|
||||||
|
const dateValue = e.target.value
|
||||||
|
setDueDateOnly(dateValue)
|
||||||
|
if (useCustomTime && dueTime) {
|
||||||
|
setDueDate(
|
||||||
|
moment(`${dateValue}T${dueTime}`).format('YYYY-MM-DDTHH:mm:00'),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setDueDate(moment(dateValue).endOf('day').format('YYYY-MM-DDTHH:mm:ss'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDueTimeChange = e => {
|
||||||
|
const timeValue = e.target.value
|
||||||
|
setDueTime(timeValue)
|
||||||
|
if (dueDateOnly) {
|
||||||
|
setDueDate(
|
||||||
|
moment(`${dueDateOnly}T${timeValue}`).format('YYYY-MM-DDTHH:mm:00'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUseCustomTimeChange = checked => {
|
||||||
|
setUseCustomTime(checked)
|
||||||
|
if (checked) {
|
||||||
|
const defaultTime = dueTime || '18:00'
|
||||||
|
setDueTime(defaultTime)
|
||||||
|
if (dueDateOnly) {
|
||||||
|
setDueDate(
|
||||||
|
moment(`${dueDateOnly}T${defaultTime}`).format('YYYY-MM-DDTHH:mm:00'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dueDateOnly) {
|
||||||
|
setDueDate(
|
||||||
|
moment(dueDateOnly).endOf('day').format('YYYY-MM-DDTHH:mm:ss'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleEnterPressed = () => {
|
const handleEnterPressed = () => {
|
||||||
createChore()
|
createChore()
|
||||||
}
|
}
|
||||||
@@ -496,6 +565,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
setProjectId(getInitialProject())
|
setProjectId(getInitialProject())
|
||||||
setHasDeadline(false)
|
setHasDeadline(false)
|
||||||
setDeadlineOffset(-1)
|
setDeadlineOffset(-1)
|
||||||
|
setDueDateOnly(null)
|
||||||
|
setDueTime(null)
|
||||||
|
setUseCustomTime(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createChore = () => {
|
const createChore = () => {
|
||||||
@@ -526,6 +598,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
|
|
||||||
const chore = {
|
const chore = {
|
||||||
name: taskTitle,
|
name: taskTitle,
|
||||||
|
description: description,
|
||||||
assignees: finalAssignees,
|
assignees: finalAssignees,
|
||||||
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||||
assignedTo: finalAssignedTo,
|
assignedTo: finalAssignedTo,
|
||||||
@@ -781,7 +854,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
variant='plain'
|
variant='plain'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'))
|
const tomorrow = moment().add(1, 'day')
|
||||||
|
setDueDateOnly(tomorrow.format('YYYY-MM-DD'))
|
||||||
|
setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:ss'))
|
||||||
|
setUseCustomTime(false)
|
||||||
|
setDueTime(null)
|
||||||
}}
|
}}
|
||||||
endDecorator={
|
endDecorator={
|
||||||
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='B' />
|
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='B' />
|
||||||
@@ -804,7 +881,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
Edit Notifications
|
Edit Notifications
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!hasDeadline && dueDate && (
|
{/* {!hasDeadline && dueDate && (
|
||||||
<Button
|
<Button
|
||||||
startDecorator={<Add />}
|
startDecorator={<Add />}
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -816,7 +893,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
>
|
>
|
||||||
Set Deadline
|
Set Deadline
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{hasDescription && (
|
{hasDescription && (
|
||||||
@@ -871,11 +948,30 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography level='body-sm'>Due Date</Typography>
|
<Typography level='body-sm'>Due Date</Typography>
|
||||||
<Input
|
<Input
|
||||||
type='datetime-local'
|
type='date'
|
||||||
value={dueDate}
|
value={dueDateOnly || ''}
|
||||||
onChange={e => setDueDate(e.target.value)}
|
onChange={handleDueDateChange}
|
||||||
sx={{ width: '100%', fontSize: '16px' }}
|
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
size='sm'
|
||||||
|
checked={useCustomTime}
|
||||||
|
onChange={e => handleUseCustomTimeChange(e.target.checked)}
|
||||||
|
label='Set a specific time'
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
{useCustomTime
|
||||||
|
? 'Task will be due at the specified time'
|
||||||
|
: 'Task will be due at the end of the day (11:59 PM)'}
|
||||||
|
</FormHelperText>
|
||||||
|
{useCustomTime && (
|
||||||
|
<Input
|
||||||
|
type='time'
|
||||||
|
value={dueTime || '18:00'}
|
||||||
|
onChange={handleDueTimeChange}
|
||||||
|
sx={{ maxWidth: 200, mt: 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -978,7 +1074,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</FormControl> */}
|
</FormControl> */}
|
||||||
{hasDeadline && dueDate && (
|
{/* {hasDeadline && dueDate && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -998,7 +1094,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
<Typography level='body-sm'>after due date</Typography>
|
<Typography level='body-sm'>after due date</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)} */}
|
||||||
{hasNotifications && dueDate && (
|
{hasNotifications && dueDate && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -1013,11 +1109,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
if (
|
if (
|
||||||
metadata.notifications !== notificationMetadata.templates
|
metadata.notifications !== notificationMetadata.templates
|
||||||
) {
|
) {
|
||||||
const newNotificaitonMetadata = {
|
const newNotificationMetadata = {
|
||||||
...notificationMetadata,
|
...notificationMetadata,
|
||||||
templates: metadata.notifications,
|
templates: metadata.notifications,
|
||||||
}
|
}
|
||||||
setNotificationMetadata(newNotificaitonMetadata)
|
setNotificationMetadata(newNotificationMetadata)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={notificationMetadata}
|
value={notificationMetadata}
|
||||||
|
|||||||
@@ -722,8 +722,16 @@ export const parseDueDate = (inputSentence, chrono) => {
|
|||||||
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
|
// If no specific time was mentioned, set to end of day (23:59:59)
|
||||||
|
// to indicate the date has no specific time tied to it (same convention as ChoreEdit)
|
||||||
|
let resultDate = dueDateMatch.start.date()
|
||||||
|
if (!dueDateMatch.start.isCertain('hour')) {
|
||||||
|
resultDate = new Date(resultDate)
|
||||||
|
resultDate.setHours(23, 59, 59, 0)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: dueDateMatch.start.date(),
|
result: resultDate,
|
||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: fullHighlightText,
|
text: fullHighlightText,
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ const NavBar = () => {
|
|||||||
'/login',
|
'/login',
|
||||||
'/auth/oauth2',
|
'/auth/oauth2',
|
||||||
'/forgot-password',
|
'/forgot-password',
|
||||||
|
'/password/update',
|
||||||
'/login/settings',
|
'/login/settings',
|
||||||
'/welcome',
|
'/welcome',
|
||||||
].includes(location.pathname)
|
].includes(location.pathname)
|
||||||
|
|||||||
@@ -167,6 +167,13 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Prevent iOS Safari from auto-zooming on focus (triggered when font-size < 16px) */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
.quill-root .ql-editor {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom focus styles */
|
/* Custom focus styles */
|
||||||
.quill-root:focus-within .ql-toolbar.ql-snow {
|
.quill-root:focus-within .ql-toolbar.ql-snow {
|
||||||
border-color: var(--joy-palette-primary-outlinedBorder, #1976d2);
|
border-color: var(--joy-palette-primary-outlinedBorder, #1976d2);
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ const SmartTaskTitleInput = ({
|
|||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
lineHeight: 'inherit',
|
lineHeight: 'inherit',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
|
color: 'transparent',
|
||||||
caretColor: mode === 'dark' ? '#fff' : '#000',
|
caretColor: mode === 'dark' ? '#fff' : '#000',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user