@@ -25,8 +25,8 @@ export const getDueDateChipText = (nextDueDate, chore) => {
|
||||
const dueDate = moment(nextDueDate)
|
||||
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
||||
|
||||
// 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) {
|
||||
// For overdue dates, show calendar format for recent dates
|
||||
const absDiff = Math.abs(diff)
|
||||
|
||||
@@ -16,6 +16,8 @@ export const ChoreHistoryStatus = Object.freeze({
|
||||
SKIPPED: 2,
|
||||
PENDING_APPROVAL: 3,
|
||||
REJECTED: 4,
|
||||
MISSED: 5,
|
||||
RESCHEDULED: 6,
|
||||
})
|
||||
export const ChoreStatus = Object.freeze({
|
||||
INACTIVE: 0,
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Add, HorizontalRule, Save } from '@mui/icons-material'
|
||||
import { Add, ArrowDropDown, HorizontalRule, Save } from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Container,
|
||||
Divider,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
IconButton,
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
Option,
|
||||
Radio,
|
||||
@@ -291,7 +296,7 @@ const ChoreEdit = () => {
|
||||
|
||||
if (dueDateOnly) {
|
||||
const combinedDateTime = moment(`${dueDateOnly}T${defaultTime}`).format(
|
||||
'YYYY-MM-DDTHH:mm:00',
|
||||
'YYYY-MM-DDTHH:mm:59',
|
||||
)
|
||||
setDueDate(combinedDateTime)
|
||||
|
||||
@@ -309,7 +314,7 @@ const ChoreEdit = () => {
|
||||
if (dueDateOnly) {
|
||||
const endOfDay = moment(dueDateOnly)
|
||||
.endOf('day')
|
||||
.format('YYYY-MM-DDTHH:mm:00')
|
||||
.format('YYYY-MM-DDTHH:mm:ss')
|
||||
setDueDate(endOfDay)
|
||||
}
|
||||
}
|
||||
@@ -558,7 +563,7 @@ const ChoreEdit = () => {
|
||||
const today = moment(new Date()).format('YYYY-MM-DD')
|
||||
setDueDateOnly(today)
|
||||
// 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)
|
||||
setDueTime(null)
|
||||
}
|
||||
@@ -1107,7 +1112,7 @@ const ChoreEdit = () => {
|
||||
const today = moment(new Date()).format('YYYY-MM-DD')
|
||||
setDueDateOnly(today)
|
||||
setDueDate(
|
||||
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'),
|
||||
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'),
|
||||
)
|
||||
setUseCustomTime(false)
|
||||
setDueTime(null)
|
||||
@@ -1638,39 +1643,38 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
{choreId > 0 && (
|
||||
<>
|
||||
{isActive ? (
|
||||
<Button
|
||||
color='danger'
|
||||
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()
|
||||
}}
|
||||
<Dropdown>
|
||||
<ButtonGroup
|
||||
variant='outlined'
|
||||
color={isActive ? 'danger' : 'neutral'}
|
||||
>
|
||||
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
|
||||
color='neutral'
|
||||
|
||||
@@ -671,7 +671,6 @@ const ChoreView = () => {
|
||||
color='neutral'
|
||||
variant='plain'
|
||||
fullWidth
|
||||
disabled={chore.isActive === false}
|
||||
onClick={() => {
|
||||
navigate(`/chores/${choreId}/history`)
|
||||
}}
|
||||
@@ -690,7 +689,6 @@ const ChoreView = () => {
|
||||
color='neutral'
|
||||
variant='plain'
|
||||
fullWidth
|
||||
disabled={chore.isActive === false}
|
||||
sx={{
|
||||
// top right of the card:
|
||||
flexDirection: 'column',
|
||||
@@ -1016,9 +1014,7 @@ const ChoreView = () => {
|
||||
size='lg'
|
||||
onClick={handleTaskCompletion}
|
||||
disabled={
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
notInCompletionWindow(chore) || chore.isActive === false
|
||||
}
|
||||
color='success'
|
||||
startDecorator={<Check />}
|
||||
@@ -1050,9 +1046,7 @@ const ChoreView = () => {
|
||||
})
|
||||
}}
|
||||
disabled={
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
notInCompletionWindow(chore) || chore.isActive === false
|
||||
}
|
||||
startDecorator={<SwitchAccessShortcut />}
|
||||
sx={{
|
||||
@@ -1081,8 +1075,7 @@ const ChoreView = () => {
|
||||
disabled={
|
||||
(chore.status === ChoreStatus.PAUSED &&
|
||||
notInCompletionWindow(chore)) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
chore.isActive === false
|
||||
}
|
||||
chore={chore}
|
||||
onAction={action => {
|
||||
@@ -1108,9 +1101,7 @@ const ChoreView = () => {
|
||||
variant='soft'
|
||||
color='success'
|
||||
disabled={
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
notInCompletionWindow(chore) || chore.isActive === false
|
||||
}
|
||||
startDecorator={<PlayArrow />}
|
||||
sx={{
|
||||
|
||||
@@ -76,9 +76,9 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
|
||||
icon: <Timelapse />,
|
||||
}
|
||||
} else if (activity.status === 1) {
|
||||
const wasOnTime = moment(activity.performedAt).isSameOrBefore(
|
||||
moment(activity.dueDate),
|
||||
)
|
||||
const wasOnTime =
|
||||
!activity.dueDate ||
|
||||
moment(activity.performedAt).isSameOrBefore(moment(activity.dueDate))
|
||||
|
||||
if (wasOnTime) {
|
||||
return {
|
||||
|
||||
@@ -221,15 +221,12 @@ const MyChores = () => {
|
||||
let choresToGroup = chores
|
||||
if (tempFilter || activeFilterId) {
|
||||
choresToGroup = customFilteredChores
|
||||
} else if (selectedProject) {
|
||||
// Otherwise, use project-filtered chores for section grouping
|
||||
if (selectedProject.id === 'default') {
|
||||
// Default project: only show tasks without a projectId
|
||||
choresToGroup = chores.filter(chore => !chore.projectId)
|
||||
} else {
|
||||
// Other projects: use the existing filter function
|
||||
choresToGroup = filterByProject(chores, selectedProject.id)
|
||||
}
|
||||
} else if (!selectedProject || selectedProject.id === 'default') {
|
||||
// No project selected or default project: only show tasks without a projectId
|
||||
choresToGroup = chores.filter(chore => !chore.projectId)
|
||||
} else {
|
||||
// Specific project: use the existing filter function
|
||||
choresToGroup = filterByProject(chores, selectedProject.id)
|
||||
}
|
||||
|
||||
const sections = ChoresGrouper(
|
||||
@@ -529,10 +526,11 @@ const MyChores = () => {
|
||||
onEnableMultiSelectAndSelectAll: () => {
|
||||
toggleMultiSelectMode()
|
||||
setTimeout(() => {
|
||||
selectAllVisibleChores()
|
||||
selectAllVisibleChores(null, choreSections, openChoreSections)
|
||||
}, 0)
|
||||
},
|
||||
onSelectAll: () => selectAllVisibleChores(),
|
||||
onSelectAll: () =>
|
||||
selectAllVisibleChores(null, choreSections, openChoreSections),
|
||||
onClearSelection: clearSelection,
|
||||
onBulkComplete: handleBulkComplete,
|
||||
onBulkSkip: handleBulkSkip,
|
||||
|
||||
@@ -473,7 +473,7 @@ export const useChoreActions = ({
|
||||
)
|
||||
|
||||
const handleBulkComplete = useCallback(async () => {
|
||||
const selectedData = getSelectedChoresData()
|
||||
const selectedData = getSelectedChoresData(chores)
|
||||
if (selectedData.length === 0) return
|
||||
|
||||
setConfirmModelConfig({
|
||||
@@ -533,7 +533,7 @@ export const useChoreActions = ({
|
||||
}, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||
|
||||
const handleBulkArchive = useCallback(async () => {
|
||||
const selectedData = getSelectedChoresData()
|
||||
const selectedData = getSelectedChoresData(chores)
|
||||
if (selectedData.length === 0) return
|
||||
|
||||
setConfirmModelConfig({
|
||||
@@ -595,7 +595,7 @@ export const useChoreActions = ({
|
||||
}, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||
|
||||
const handleBulkDelete = useCallback(async () => {
|
||||
const selectedData = getSelectedChoresData()
|
||||
const selectedData = getSelectedChoresData(chores)
|
||||
if (selectedData.length === 0) return
|
||||
|
||||
setConfirmModelConfig({
|
||||
@@ -655,7 +655,7 @@ export const useChoreActions = ({
|
||||
}, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||
|
||||
const handleBulkSkip = useCallback(async () => {
|
||||
const selectedData = getSelectedChoresData()
|
||||
const selectedData = getSelectedChoresData(chores)
|
||||
if (selectedData.length === 0) return
|
||||
|
||||
setConfirmModelConfig({
|
||||
|
||||
@@ -15,9 +15,7 @@ export const useChoreFilters = ({
|
||||
)
|
||||
|
||||
const projectFilteredChores = useMemo(() => {
|
||||
if (!selectedProject) return chores
|
||||
|
||||
if (selectedProject.id === 'default') {
|
||||
if (!selectedProject || selectedProject.id === 'default') {
|
||||
return chores.filter(chore => !chore.projectId)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ const ChoreHistory = () => {
|
||||
{
|
||||
icon: <Checklist />,
|
||||
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 />,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Person,
|
||||
Redo,
|
||||
RunningWithErrors,
|
||||
Schedule,
|
||||
ThumbDown,
|
||||
Timelapse,
|
||||
Toll,
|
||||
@@ -17,7 +18,7 @@ import moment from 'moment'
|
||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
|
||||
const getCompletedChip = historyEntry => {
|
||||
if (historyEntry.status === 0 || historyEntry.status === 5) {
|
||||
if (historyEntry.status === 0 || historyEntry.status === 5 || historyEntry.status === 6) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -126,6 +127,7 @@ const HistoryCard = ({
|
||||
3: { icon: <HourglassEmpty />, color: 'neutral' }, // Pending Approval
|
||||
4: { icon: <ThumbDown />, color: 'danger' }, // Rejected
|
||||
5: { icon: <RunningWithErrors />, color: 'danger' }, // Missed
|
||||
6: { icon: <Schedule />, color: 'warning' }, // Rescheduled
|
||||
}
|
||||
|
||||
const config = statusMap[historyEntry.status] || statusMap[1]
|
||||
@@ -192,7 +194,9 @@ const HistoryCard = ({
|
||||
? 'Rejected'
|
||||
: historyEntry.status === 5
|
||||
? 'Missed'
|
||||
: 'Completed'}
|
||||
: historyEntry.status === 6
|
||||
? 'Rescheduled'
|
||||
: 'Completed'}
|
||||
</Typography>
|
||||
{historyEntry.performedAt && (
|
||||
<Chip size='sm' startDecorator={<EventNote />}>
|
||||
|
||||
@@ -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 {
|
||||
Block,
|
||||
AccessTime,
|
||||
Check,
|
||||
EventBusy,
|
||||
EventNote,
|
||||
Group,
|
||||
HourglassEmpty,
|
||||
Redo,
|
||||
RunningWithErrors,
|
||||
Schedule,
|
||||
ThumbDown,
|
||||
Timeline,
|
||||
Toll,
|
||||
} from '@mui/icons-material'
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { ChoresGrouper } from '../../utils/Chores'
|
||||
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
@@ -43,7 +46,9 @@ const groupByDate = history => {
|
||||
const aggregated = {}
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const item = history[i]
|
||||
const date = new Date(item.performedAt).toLocaleDateString()
|
||||
const date = new Date(
|
||||
item.performedAt || item.updatedAt,
|
||||
).toLocaleDateString()
|
||||
if (!aggregated[date]) {
|
||||
aggregated[date] = []
|
||||
}
|
||||
@@ -52,21 +57,25 @@ const groupByDate = history => {
|
||||
return aggregated
|
||||
}
|
||||
|
||||
const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
|
||||
const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onViewNote }) => {
|
||||
const getStatusIcon = status => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return <TimelapseIcon color='primary' />
|
||||
return <AccessTime color='primary' />
|
||||
case 1:
|
||||
return <Check color='success' />
|
||||
case 2:
|
||||
return <Block color='warning' />
|
||||
return <Redo color='warning' />
|
||||
case 3:
|
||||
return <HourglassEmptyIcon color='action' />
|
||||
return <HourglassEmpty color='neutral' />
|
||||
case 4:
|
||||
return <ThumbDownIcon color='error' />
|
||||
return <ThumbDown color='error' />
|
||||
case 5:
|
||||
return <RunningWithErrors color='error' />
|
||||
case 6:
|
||||
return <Schedule color='warning' />
|
||||
default:
|
||||
return <CheckCircleIcon color='success' />
|
||||
return <Check color='success' />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,14 +122,33 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
|
||||
{`${points} points`}
|
||||
</Chip>
|
||||
)}
|
||||
{notes && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
startDecorator={<EventNote />}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onViewNote?.(notes)
|
||||
}}
|
||||
>
|
||||
Note
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const ChoreHistoryTimeline = ({ history }) => {
|
||||
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||
const groupedHistory = groupByDate(history)
|
||||
|
||||
const sortedEntries = Object.entries(groupedHistory).sort(
|
||||
([a], [b]) => new Date(b) - new Date(a),
|
||||
)
|
||||
|
||||
return (
|
||||
<Container sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
@@ -146,13 +174,17 @@ const ChoreHistoryTimeline = ({ history }) => {
|
||||
<>
|
||||
<ChoreHistoryItem
|
||||
key={record.id}
|
||||
time={new Date(record.performedAt).toLocaleTimeString([], {
|
||||
time={new Date(
|
||||
record.performedAt || record.updatedAt,
|
||||
).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
name={record.choreName}
|
||||
points={record.points}
|
||||
status={record.status}
|
||||
notes={record.notes}
|
||||
onViewNote={onViewNote}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
@@ -372,6 +404,7 @@ const UserActivites = () => {
|
||||
const [selectedHistory, setSelectedHistory] = React.useState([])
|
||||
const [enrichedHistory, setEnrichedHistory] = React.useState([])
|
||||
const [selectedChart, setSelectedChart] = React.useState('history')
|
||||
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||
|
||||
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
|
||||
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
|
||||
@@ -1072,7 +1105,17 @@ const UserActivites = () => {
|
||||
>
|
||||
{/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */}
|
||||
<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>
|
||||
|
||||
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
|
||||
@@ -1222,6 +1265,7 @@ const UserActivites = () => {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<NoteViewerModal config={noteViewerConfig} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
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 * as chrono from 'chrono-node'
|
||||
import moment from 'moment'
|
||||
@@ -92,6 +101,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const [hasNotifications, setHasNotifications] = useState(false)
|
||||
const [hasDeadline, setHasDeadline] = useState(false)
|
||||
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 [projectId, setProjectId] = useState(getInitialProject())
|
||||
|
||||
@@ -135,7 +147,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
!dueDate
|
||||
) {
|
||||
// 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)
|
||||
}
|
||||
// Enter key to create task
|
||||
@@ -379,9 +395,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
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
|
||||
if (dueDateParsed.result) {
|
||||
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
|
||||
syncDueDateStates(dueDateParsed.result)
|
||||
dueDateHighlight = dueDateParsed.highlight[0]
|
||||
}
|
||||
|
||||
@@ -390,9 +421,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
// we need to reparse the date again to get the correct due date:
|
||||
const dueDateParsedAgain = parseDueDate(sentence, chrono)
|
||||
if (dueDateParsedAgain.result) {
|
||||
setDueDate(
|
||||
moment(dueDateParsedAgain.result).format('YYYY-MM-DDTHH:mm:ss'),
|
||||
)
|
||||
syncDueDateStates(dueDateParsedAgain.result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,6 +501,47 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
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 = () => {
|
||||
createChore()
|
||||
}
|
||||
@@ -495,6 +565,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
setProjectId(getInitialProject())
|
||||
setHasDeadline(false)
|
||||
setDeadlineOffset(-1)
|
||||
setDueDateOnly(null)
|
||||
setDueTime(null)
|
||||
setUseCustomTime(false)
|
||||
}
|
||||
|
||||
const createChore = () => {
|
||||
@@ -525,6 +598,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
|
||||
const chore = {
|
||||
name: taskTitle,
|
||||
description: description,
|
||||
assignees: finalAssignees,
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||
assignedTo: finalAssignedTo,
|
||||
@@ -780,7 +854,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
variant='plain'
|
||||
size='sm'
|
||||
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={
|
||||
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='B' />
|
||||
@@ -870,11 +948,30 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Due Date</Typography>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
value={dueDate}
|
||||
onChange={e => setDueDate(e.target.value)}
|
||||
sx={{ width: '100%', fontSize: '16px' }}
|
||||
type='date'
|
||||
value={dueDateOnly || ''}
|
||||
onChange={handleDueDateChange}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -722,8 +722,16 @@ export const parseDueDate = (inputSentence, chrono) => {
|
||||
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
||||
.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 {
|
||||
result: dueDateMatch.start.date(),
|
||||
result: resultDate,
|
||||
highlight: [
|
||||
{
|
||||
text: fullHighlightText,
|
||||
|
||||
@@ -171,6 +171,7 @@ const NavBar = () => {
|
||||
'/login',
|
||||
'/auth/oauth2',
|
||||
'/forgot-password',
|
||||
'/password/update',
|
||||
'/login/settings',
|
||||
'/welcome',
|
||||
].includes(location.pathname)
|
||||
|
||||
@@ -213,7 +213,7 @@ const SmartTaskTitleInput = ({
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
|
||||
color: 'transparent',
|
||||
caretColor: mode === 'dark' ? '#fff' : '#000',
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user