Bump version to 0.1.95 and add dnd-kit dependencies; implement CompleteSubTask function and enhance chore sorting logic
This commit is contained in:
parent
e359b1c0a4
commit
bbea27d380
9 changed files with 511 additions and 135 deletions
59
package-lock.json
generated
59
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.91",
|
"version": "0.1.94",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.91",
|
"version": "0.1.94",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.1.1",
|
"@capacitor/android": "^6.1.1",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
|
@ -18,6 +18,8 @@
|
||||||
"@capacitor/preferences": "^6.0.1",
|
"@capacitor/preferences": "^6.0.1",
|
||||||
"@capacitor/push-notifications": "^6.0.1",
|
"@capacitor/push-notifications": "^6.0.1",
|
||||||
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.15.2",
|
"@mui/icons-material": "^5.15.2",
|
||||||
|
@ -1804,6 +1806,59 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emotion/babel-plugin": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.13.5",
|
"version": "11.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.94",
|
"version": "0.1.95",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
@ -32,6 +32,8 @@
|
||||||
"@capacitor/preferences": "^6.0.1",
|
"@capacitor/preferences": "^6.0.1",
|
||||||
"@capacitor/push-notifications": "^6.0.1",
|
"@capacitor/push-notifications": "^6.0.1",
|
||||||
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.15.2",
|
"@mui/icons-material": "^5.15.2",
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { TASK_COLOR } from './Colors.jsx'
|
import { TASK_COLOR } from './Colors.jsx'
|
||||||
|
|
||||||
|
const priorityOrder = [1, 2, 3, 4, 0]
|
||||||
|
|
||||||
export const ChoresGrouper = (groupBy, chores) => {
|
export const ChoresGrouper = (groupBy, chores) => {
|
||||||
// sort by priority then due date:
|
// sort by priority then due date:
|
||||||
chores.sort((a, b) => {
|
chores.sort(ChoreSorter)
|
||||||
// no priority is lowest priority:
|
|
||||||
if (a.priority === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if (a.priority !== b.priority) {
|
|
||||||
return a.priority - b.priority
|
|
||||||
}
|
|
||||||
if (a.nextDueDate === null) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if (b.nextDueDate === null) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
|
|
||||||
})
|
|
||||||
|
|
||||||
var groups = []
|
var groups = []
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case 'due_date':
|
case 'due_date':
|
||||||
|
@ -159,7 +145,25 @@ export const ChoresGrouper = (groupBy, chores) => {
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
export const ChoreSorter = (a, b) => {
|
||||||
|
const priorityA = priorityOrder.indexOf(a.priority)
|
||||||
|
const priorityB = priorityOrder.indexOf(b.priority)
|
||||||
|
if (priorityA !== priorityB) {
|
||||||
|
return priorityA - priorityB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status sorting (0 > 1 > ... ascending order)
|
||||||
|
if (a.status !== b.status) {
|
||||||
|
return a.status - b.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Due date sorting (earlier dates first, null/undefined last)
|
||||||
|
if (!a.nextDueDate && !b.nextDueDate) return 0
|
||||||
|
if (!a.nextDueDate) return 1
|
||||||
|
if (!b.nextDueDate) return -1
|
||||||
|
|
||||||
|
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
|
||||||
|
}
|
||||||
export const notInCompletionWindow = chore => {
|
export const notInCompletionWindow = chore => {
|
||||||
return (
|
return (
|
||||||
chore.completionWindow &&
|
chore.completionWindow &&
|
||||||
|
|
|
@ -126,6 +126,15 @@ const MarkChoreComplete = (id, note, completedDate, performer) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CompleteSubTask = (id, choreId, completedAt) => {
|
||||||
|
var markChoreURL = `/chores/${choreId}/subtask`
|
||||||
|
return Fetch(markChoreURL, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: HEADERS(),
|
||||||
|
body: JSON.stringify({ completedAt, id, choreId }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const SkipChore = id => {
|
const SkipChore = id => {
|
||||||
return Fetch(`/chores/${id}/skip`, {
|
return Fetch(`/chores/${id}/skip`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -476,6 +485,7 @@ export {
|
||||||
ArchiveChore,
|
ArchiveChore,
|
||||||
CancelSubscription,
|
CancelSubscription,
|
||||||
ChangePassword,
|
ChangePassword,
|
||||||
|
CompleteSubTask,
|
||||||
CreateChore,
|
CreateChore,
|
||||||
CreateLabel,
|
CreateLabel,
|
||||||
CreateLongLiveToken,
|
CreateLongLiveToken,
|
||||||
|
|
|
@ -7,16 +7,10 @@ import {
|
||||||
|
|
||||||
const Priorities = [
|
const Priorities = [
|
||||||
{
|
{
|
||||||
name: 'P4',
|
name: 'P1',
|
||||||
value: 4,
|
value: 1,
|
||||||
icon: <HorizontalRule />,
|
icon: <PriorityHigh />,
|
||||||
color: '',
|
color: 'danger',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'P3 ',
|
|
||||||
value: 3,
|
|
||||||
icon: <KeyboardControlKey />,
|
|
||||||
color: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P2',
|
name: 'P2',
|
||||||
|
@ -25,10 +19,16 @@ const Priorities = [
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P1',
|
name: 'P3 ',
|
||||||
value: 1,
|
value: 3,
|
||||||
icon: <PriorityHigh />,
|
icon: <KeyboardControlKey />,
|
||||||
color: 'danger',
|
color: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P4',
|
||||||
|
value: 4,
|
||||||
|
icon: <HorizontalRule />,
|
||||||
|
color: '',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
FormLabel,
|
|
||||||
Input,
|
Input,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
@ -40,6 +39,8 @@ import {
|
||||||
SaveChore,
|
SaveChore,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
|
import Priorities from '../../utils/Priorities.jsx'
|
||||||
|
import SubTasks from '../components/SubTask.jsx'
|
||||||
import { useLabels } from '../Labels/LabelQueries'
|
import { useLabels } from '../Labels/LabelQueries'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
import LabelModal from '../Modals/Inputs/LabelModal'
|
import LabelModal from '../Modals/Inputs/LabelModal'
|
||||||
|
@ -76,7 +77,9 @@ const ChoreEdit = () => {
|
||||||
const [frequencyMetadata, setFrequencyMetadata] = useState({})
|
const [frequencyMetadata, setFrequencyMetadata] = useState({})
|
||||||
const [labels, setLabels] = useState([])
|
const [labels, setLabels] = useState([])
|
||||||
const [labelsV2, setLabelsV2] = useState([])
|
const [labelsV2, setLabelsV2] = useState([])
|
||||||
|
const [priority, setPriority] = useState(0)
|
||||||
const [points, setPoints] = useState(-1)
|
const [points, setPoints] = useState(-1)
|
||||||
|
const [subTasks, setSubTasks] = useState(null)
|
||||||
const [completionWindow, setCompletionWindow] = useState(-1)
|
const [completionWindow, setCompletionWindow] = useState(-1)
|
||||||
const [allUserThings, setAllUserThings] = useState([])
|
const [allUserThings, setAllUserThings] = useState([])
|
||||||
const [thingTrigger, setThingTrigger] = useState(null)
|
const [thingTrigger, setThingTrigger] = useState(null)
|
||||||
|
@ -201,10 +204,12 @@ const ChoreEdit = () => {
|
||||||
notification: isNotificable,
|
notification: isNotificable,
|
||||||
labels: labels.map(l => l.name),
|
labels: labels.map(l => l.name),
|
||||||
labelsV2: labelsV2,
|
labelsV2: labelsV2,
|
||||||
|
subTasks: subTasks,
|
||||||
notificationMetadata: notificationMetadata,
|
notificationMetadata: notificationMetadata,
|
||||||
thingTrigger: thingTrigger,
|
thingTrigger: thingTrigger,
|
||||||
points: points < 0 ? null : points,
|
points: points < 0 ? null : points,
|
||||||
completionWindow: completionWindow < 0 ? null : completionWindow,
|
completionWindow: completionWindow < 0 ? null : completionWindow,
|
||||||
|
priority: priority,
|
||||||
}
|
}
|
||||||
let SaveFunction = CreateChore
|
let SaveFunction = CreateChore
|
||||||
if (choreId > 0) {
|
if (choreId > 0) {
|
||||||
|
@ -265,6 +270,8 @@ const ChoreEdit = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
setLabelsV2(data.res.labelsV2)
|
setLabelsV2(data.res.labelsV2)
|
||||||
|
setSubTasks(data.res.subTasks)
|
||||||
|
setPriority(data.res.priority)
|
||||||
setAssignStrategy(
|
setAssignStrategy(
|
||||||
data.res.assignStrategy
|
data.res.assignStrategy
|
||||||
? data.res.assignStrategy
|
? data.res.assignStrategy
|
||||||
|
@ -382,7 +389,7 @@ const ChoreEdit = () => {
|
||||||
</Typography> */}
|
</Typography> */}
|
||||||
<Box>
|
<Box>
|
||||||
<FormControl error={errors.name}>
|
<FormControl error={errors.name}>
|
||||||
<Typography level='h4'>Title :</Typography>
|
<Typography level='h4'>Name :</Typography>
|
||||||
<Typography level='h5'> What is the name of this chore?</Typography>
|
<Typography level='h5'> What is the name of this chore?</Typography>
|
||||||
<Input value={name} onChange={e => setName(e.target.value)} />
|
<Input value={name} onChange={e => setName(e.target.value)} />
|
||||||
<FormHelperText error>{errors.name}</FormHelperText>
|
<FormHelperText error>{errors.name}</FormHelperText>
|
||||||
|
@ -390,8 +397,8 @@ const ChoreEdit = () => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<FormControl error={errors.description}>
|
<FormControl error={errors.description}>
|
||||||
<Typography level='h4'>Details:</Typography>
|
<Typography level='h4'>Additional Details :</Typography>
|
||||||
<Typography level='h5'>What is this chore about?</Typography>
|
<Typography level='h5'>What is this task about?</Typography>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
@ -401,7 +408,7 @@ const ChoreEdit = () => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Typography level='h4'>Assignees :</Typography>
|
<Typography level='h4'>Assignees :</Typography>
|
||||||
<Typography level='h5'>Who can do this chore?</Typography>
|
<Typography level='h5'>Who can do this task?</Typography>
|
||||||
<Card>
|
<Card>
|
||||||
<List
|
<List
|
||||||
orientation='horizontal'
|
orientation='horizontal'
|
||||||
|
@ -590,18 +597,7 @@ const ChoreEdit = () => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormControl
|
<FormControl orientation='horizontal'>
|
||||||
orientation='horizontal'
|
|
||||||
sx={{ width: 400, justifyContent: 'space-between' }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{/* <FormLabel>Completion window (hours)</FormLabel> */}
|
|
||||||
<Typography level='h5'>Completion window (hours)</Typography>
|
|
||||||
|
|
||||||
<FormHelperText sx={{ mt: 0 }}>
|
|
||||||
{"Set a time window that task can't be completed before"}
|
|
||||||
</FormHelperText>
|
|
||||||
</div>
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={completionWindow != -1}
|
checked={completionWindow != -1}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
|
@ -615,14 +611,18 @@ const ChoreEdit = () => {
|
||||||
color={completionWindow !== -1 ? 'success' : 'neutral'}
|
color={completionWindow !== -1 ? 'success' : 'neutral'}
|
||||||
variant={completionWindow !== -1 ? 'solid' : 'outlined'}
|
variant={completionWindow !== -1 ? 'solid' : 'outlined'}
|
||||||
// endDecorator={points !== -1 ? 'On' : 'Off'}
|
// endDecorator={points !== -1 ? 'On' : 'Off'}
|
||||||
slotProps={{
|
sx={{
|
||||||
endDecorator: {
|
mr: 2,
|
||||||
sx: {
|
|
||||||
minWidth: 24,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
{/* <FormLabel>Completion window (hours)</FormLabel> */}
|
||||||
|
<Typography level='h5'>Completion window (hours)</Typography>
|
||||||
|
|
||||||
|
<FormHelperText sx={{ mt: 0 }}>
|
||||||
|
{"Set a time window that task can't be completed before"}
|
||||||
|
</FormHelperText>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{completionWindow != -1 && (
|
{completionWindow != -1 && (
|
||||||
<Card variant='outlined'>
|
<Card variant='outlined'>
|
||||||
|
@ -913,44 +913,89 @@ const ChoreEdit = () => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box mt={2}>
|
||||||
|
<Typography level='h4'>Priority :</Typography>
|
||||||
|
<Typography level='h5'>How important is this task?</Typography>
|
||||||
|
<Select
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
setPriority(newValue)
|
||||||
|
}}
|
||||||
|
value={priority}
|
||||||
|
sx={{ minWidth: '15rem' }}
|
||||||
|
slotProps={{
|
||||||
|
listbox: {
|
||||||
|
sx: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Priorities.map(priority => (
|
||||||
|
<Option key={priority.id + priority.name} value={priority.value}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '20 px',
|
||||||
|
height: '20 px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: priority.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{priority.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
<Option value={0}>No Priority</Option>
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Typography level='h4' gutterBottom>
|
<Typography level='h4' gutterBottom>
|
||||||
Others :
|
Others :
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControl
|
<FormControl sx={{ mt: 1 }}>
|
||||||
orientation='horizontal'
|
<Checkbox
|
||||||
sx={{ width: 400, justifyContent: 'space-between' }}
|
onChange={e => {
|
||||||
>
|
if (e.target.checked) {
|
||||||
<div>
|
setSubTasks([])
|
||||||
<FormLabel>Assign Points</FormLabel>
|
|
||||||
<FormHelperText sx={{ mt: 0 }}>
|
|
||||||
Assign points to this task and user will earn points when they
|
|
||||||
completed it
|
|
||||||
</FormHelperText>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={points > -1}
|
|
||||||
onClick={event => {
|
|
||||||
event.preventDefault()
|
|
||||||
if (points > -1) {
|
|
||||||
setPoints(-1)
|
|
||||||
} else {
|
} else {
|
||||||
setPoints(1)
|
setSubTasks(null)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
color={points !== -1 ? 'success' : 'neutral'}
|
overlay
|
||||||
variant={points !== -1 ? 'solid' : 'outlined'}
|
checked={subTasks != null}
|
||||||
// endDecorator={points !== -1 ? 'On' : 'Off'}
|
value={subTasks != null}
|
||||||
slotProps={{
|
label='Sub Tasks'
|
||||||
endDecorator: {
|
|
||||||
sx: {
|
|
||||||
minWidth: 24,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<FormHelperText>Add sub tasks to this task</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
{subTasks != null && (
|
||||||
|
<Card
|
||||||
|
variant='outlined'
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SubTasks editMode={true} tasks={subTasks} setTasks={setSubTasks} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
<FormControl sx={{ mt: 1 }}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setPoints(1)
|
||||||
|
} else {
|
||||||
|
setPoints(-1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
checked={points > -1}
|
||||||
|
value={points > -1}
|
||||||
|
overlay
|
||||||
|
label='Assign Points'
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Assign points to this task and user will earn points when they
|
||||||
|
completed it
|
||||||
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{points != -1 && (
|
{points != -1 && (
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import Priorities from '../../utils/Priorities'
|
import Priorities from '../../utils/Priorities'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
|
import SubTasks from '../components/SubTask.jsx'
|
||||||
const IconCard = styled('div')({
|
const IconCard = styled('div')({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -483,13 +484,33 @@ const ChoreView = () => {
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
Previous note:
|
Previous note:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Sheet variant='plain' sx={{ p: 2, borderRadius: 'lg' }}>
|
<Sheet variant='plain' sx={{ p: 2, borderRadius: 'lg', mb: 1 }}>
|
||||||
<Typography level='body-md' sx={{ mb: 1 }}>
|
<Typography level='body-md' sx={{ mb: 1 }}>
|
||||||
{chore.notes || '--'}
|
{chore.notes || '--'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{chore.subTasks && chore.subTasks.length > 0 && (
|
||||||
|
<Box sx={{ p: 0, m: 0, mb: 2 }}>
|
||||||
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
|
Subtasks :
|
||||||
|
</Typography>
|
||||||
|
<Sheet variant='plain' sx={{ borderRadius: 'lg', p: 1 }}>
|
||||||
|
<SubTasks
|
||||||
|
editMode={false}
|
||||||
|
tasks={chore.subTasks}
|
||||||
|
setTasks={tasks => {
|
||||||
|
setChore({
|
||||||
|
...chore,
|
||||||
|
subTasks: tasks,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
choreId={choreId}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
|
@ -497,7 +518,6 @@ const ChoreView = () => {
|
||||||
p: 2,
|
p: 2,
|
||||||
borderRadius: 'md',
|
borderRadius: 'md',
|
||||||
boxShadow: 'sm',
|
boxShadow: 'sm',
|
||||||
mt: 2,
|
|
||||||
}}
|
}}
|
||||||
variant='soft'
|
variant='soft'
|
||||||
>
|
>
|
||||||
|
@ -612,7 +632,12 @@ const ChoreView = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
size='lg'
|
size='lg'
|
||||||
onClick={handleTaskCompletion}
|
onClick={handleTaskCompletion}
|
||||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
disabled={
|
||||||
|
isPendingCompletion ||
|
||||||
|
notInCompletionWindow(chore) ||
|
||||||
|
(chore.lastCompletedDate !== null &&
|
||||||
|
chore.frequencyType === 'once')
|
||||||
|
}
|
||||||
color={isPendingCompletion ? 'danger' : 'success'}
|
color={isPendingCompletion ? 'danger' : 'success'}
|
||||||
startDecorator={<Check />}
|
startDecorator={<Check />}
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -642,6 +667,9 @@ const ChoreView = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
disabled={
|
||||||
|
chore.lastCompletedDate !== null && chore.frequencyType === 'once'
|
||||||
|
}
|
||||||
startDecorator={<SwitchAccessShortcut />}
|
startDecorator={<SwitchAccessShortcut />}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
CancelRounded,
|
CancelRounded,
|
||||||
EditCalendar,
|
EditCalendar,
|
||||||
ExpandCircleDown,
|
ExpandCircleDown,
|
||||||
FilterAlt,
|
Grain,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
Search,
|
Search,
|
||||||
Sort,
|
Sort,
|
||||||
|
@ -44,7 +44,7 @@ import { useLabels } from '../Labels/LabelQueries'
|
||||||
import ChoreCard from './ChoreCard'
|
import ChoreCard from './ChoreCard'
|
||||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||||
|
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||||
import TaskInput from '../components/AddTaskModal'
|
import TaskInput from '../components/AddTaskModal'
|
||||||
import {
|
import {
|
||||||
canScheduleNotification,
|
canScheduleNotification,
|
||||||
|
@ -66,8 +66,12 @@ const MyChores = () => {
|
||||||
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
||||||
const searchInputRef = useRef()
|
const searchInputRef = useRef()
|
||||||
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
||||||
const [selectedChoreSection, setSelectedChoreSection] = useState('due_date')
|
const [selectedChoreSection, setSelectedChoreSection] = useState(
|
||||||
const [openChoreSections, setOpenChoreSections] = useState({})
|
localStorage.getItem('selectedChoreSection') || 'due_date',
|
||||||
|
)
|
||||||
|
const [openChoreSections, setOpenChoreSections] = useState(
|
||||||
|
JSON.parse(localStorage.getItem('openChoreSections')) || {},
|
||||||
|
)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [performers, setPerformers] = useState([])
|
const [performers, setPerformers] = useState([])
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
@ -75,33 +79,6 @@ const MyChores = () => {
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||||
const { data: choresData, isLoading: choresLoading } = useChores()
|
const { data: choresData, isLoading: choresLoading } = useChores()
|
||||||
const choreSorter = (a, b) => {
|
|
||||||
// 1. Handle null due dates (always last):
|
|
||||||
if (!a.nextDueDate && !b.nextDueDate) return 0 // Both null, no order
|
|
||||||
if (!a.nextDueDate) return 1 // a is null, comes later
|
|
||||||
if (!b.nextDueDate) return -1 // b is null, comes earlier
|
|
||||||
|
|
||||||
const aDueDate = new Date(a.nextDueDate)
|
|
||||||
const bDueDate = new Date(b.nextDueDate)
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
const oneDayInMs = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
// 2. Prioritize tasks due today +- 1 day:
|
|
||||||
const aTodayOrNear = Math.abs(aDueDate - now) <= oneDayInMs
|
|
||||||
const bTodayOrNear = Math.abs(bDueDate - now) <= oneDayInMs
|
|
||||||
if (aTodayOrNear && !bTodayOrNear) return -1 // a is closer
|
|
||||||
if (!aTodayOrNear && bTodayOrNear) return 1 // b is closer
|
|
||||||
|
|
||||||
// 3. Handle overdue tasks (excluding today +- 1):
|
|
||||||
const aOverdue = aDueDate < now && !aTodayOrNear
|
|
||||||
const bOverdue = bDueDate < now && !bTodayOrNear
|
|
||||||
if (aOverdue && !bOverdue) return -1 // a is overdue, comes earlier
|
|
||||||
if (!aOverdue && bOverdue) return 1 // b is overdue, comes earlier
|
|
||||||
|
|
||||||
// 4. Sort future tasks by due date:
|
|
||||||
return aDueDate - bDueDate // Sort ascending by due date
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
|
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
|
||||||
|
@ -123,7 +100,7 @@ const MyChores = () => {
|
||||||
]).then(data => {
|
]).then(data => {
|
||||||
const [choresData, usersData, userProfileData] = data
|
const [choresData, usersData, userProfileData] = data
|
||||||
setUserProfile(userProfileData.res)
|
setUserProfile(userProfileData.res)
|
||||||
choresData.res.sort(choreSorter)
|
choresData.res.sort(ChoreSorter)
|
||||||
setChores(choresData.res)
|
setChores(choresData.res)
|
||||||
setFilteredChores(choresData.res)
|
setFilteredChores(choresData.res)
|
||||||
setPerformers(usersData.res)
|
setPerformers(usersData.res)
|
||||||
|
@ -155,17 +132,20 @@ const MyChores = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (choresData) {
|
if (choresData) {
|
||||||
const sortedChores = choresData.res.sort(choreSorter)
|
const sortedChores = choresData.res.sort(ChoreSorter)
|
||||||
setChores(sortedChores)
|
setChores(sortedChores)
|
||||||
setFilteredChores(sortedChores)
|
setFilteredChores(sortedChores)
|
||||||
const sections = ChoresGrouper('due_date', sortedChores)
|
const sections = ChoresGrouper(selectedChoreSection, sortedChores)
|
||||||
setChoreSections(sections)
|
setChoreSections(sections)
|
||||||
setOpenChoreSections(
|
if (localStorage.getItem('openChoreSections') === null) {
|
||||||
Object.keys(sections).reduce((acc, key) => {
|
setSelectedChoreSectionWithCache(selectedChoreSection)
|
||||||
acc[key] = true
|
setOpenChoreSections(
|
||||||
return acc
|
Object.keys(sections).reduce((acc, key) => {
|
||||||
}, {}),
|
acc[key] = true
|
||||||
)
|
return acc
|
||||||
|
}, {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [choresData, choresLoading])
|
}, [choresData, choresLoading])
|
||||||
|
|
||||||
|
@ -184,12 +164,21 @@ const MyChores = () => {
|
||||||
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
||||||
}
|
}
|
||||||
}, [searchInputFocus])
|
}, [searchInputFocus])
|
||||||
|
const setSelectedChoreSectionWithCache = value => {
|
||||||
|
setSelectedChoreSection(value)
|
||||||
|
localStorage.setItem('selectedChoreSection', value)
|
||||||
|
}
|
||||||
|
const setOpenChoreSectionsWithCache = value => {
|
||||||
|
setOpenChoreSections(value)
|
||||||
|
localStorage.setItem('openChoreSections', JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
const updateChores = newChore => {
|
const updateChores = newChore => {
|
||||||
const newChores = chores
|
const newChores = chores
|
||||||
newChores.push(newChore)
|
newChores.push(newChore)
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
setChoreSections(ChoresGrouper('due_date', newChores))
|
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
||||||
setSelectedFilter('All')
|
setSelectedFilter('All')
|
||||||
}
|
}
|
||||||
const handleMenuOutsideClick = event => {
|
const handleMenuOutsideClick = event => {
|
||||||
|
@ -265,7 +254,7 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
setChoreSections(ChoresGrouper('due_date', newChores))
|
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
|
@ -296,7 +285,7 @@ const MyChores = () => {
|
||||||
)
|
)
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
setChoreSections(ChoresGrouper('due_date', newChores))
|
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
|
@ -443,7 +432,14 @@ const MyChores = () => {
|
||||||
onItemSelect={selected => {
|
onItemSelect={selected => {
|
||||||
const section = ChoresGrouper(selected.value, chores)
|
const section = ChoresGrouper(selected.value, chores)
|
||||||
setChoreSections(section)
|
setChoreSections(section)
|
||||||
setSelectedChoreSection(selected.value)
|
setSelectedChoreSectionWithCache(selected.value)
|
||||||
|
setOpenChoreSectionsWithCache(
|
||||||
|
// open all sections by default
|
||||||
|
Object.keys(section).reduce((acc, key) => {
|
||||||
|
acc[key] = true
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
)
|
||||||
setFilteredChores(chores)
|
setFilteredChores(chores)
|
||||||
setSelectedFilter('All')
|
setSelectedFilter('All')
|
||||||
}}
|
}}
|
||||||
|
@ -483,7 +479,7 @@ const MyChores = () => {
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFilterMenuOpen}
|
onClick={handleFilterMenuOpen}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
startDecorator={<FilterAlt />}
|
startDecorator={<Grain />}
|
||||||
color={
|
color={
|
||||||
selectedFilter &&
|
selectedFilter &&
|
||||||
FILTERS[selectedFilter] &&
|
FILTERS[selectedFilter] &&
|
||||||
|
@ -649,9 +645,9 @@ const MyChores = () => {
|
||||||
...openChoreSections,
|
...openChoreSections,
|
||||||
}
|
}
|
||||||
delete newOpenChoreSections[index]
|
delete newOpenChoreSections[index]
|
||||||
setOpenChoreSections(newOpenChoreSections)
|
setOpenChoreSectionsWithCache(newOpenChoreSections)
|
||||||
} else {
|
} else {
|
||||||
setOpenChoreSections({
|
setOpenChoreSectionsWithCache({
|
||||||
...openChoreSections,
|
...openChoreSections,
|
||||||
[index]: true,
|
[index]: true,
|
||||||
})
|
})
|
||||||
|
|
236
src/views/components/SubTask.jsx
Normal file
236
src/views/components/SubTask.jsx
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import { DndContext, closestCenter } from '@dnd-kit/core'
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
import { Add, Delete, DragIndicator, Edit } from '@mui/icons-material'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||||
|
|
||||||
|
function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id: task.id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' }, // Responsive style
|
||||||
|
touchAction: 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [editedText, setEditedText] = useState(task.name)
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setIsEditing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setIsEditing(false)
|
||||||
|
task.name = editedText
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem ref={setNodeRef} style={style} {...attributes}>
|
||||||
|
{editMode && (
|
||||||
|
<IconButton {...listeners} {...attributes}>
|
||||||
|
<DragIndicator />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!editMode && (
|
||||||
|
<Checkbox
|
||||||
|
checked={task.completedAt}
|
||||||
|
onChange={() => handleToggle(task.id)}
|
||||||
|
overlay={!editMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 50,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
value={editedText}
|
||||||
|
onChange={e => setEditedText(e.target.value)}
|
||||||
|
onBlur={handleSave}
|
||||||
|
onKeyPress={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSave()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
textDecoration: task.completedAt ? 'line-through' : 'none',
|
||||||
|
}}
|
||||||
|
onDoubleClick={handleEdit}
|
||||||
|
>
|
||||||
|
{task.name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{task.completedAt && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', sm: 'inline' }, // Responsive style
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{new Date(task.completedAt).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{editMode && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<IconButton variant='soft' onClick={handleEdit}>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant='soft'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => handleDelete(task.id)}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubTasks = ({ editMode = true, choreId = 0, tasks, setTasks }) => {
|
||||||
|
const [newTask, setNewTask] = useState('')
|
||||||
|
|
||||||
|
const handleToggle = taskId => {
|
||||||
|
const updatedTask = tasks.find(task => task.id === taskId)
|
||||||
|
updatedTask.completedAt = updatedTask.completedAt
|
||||||
|
? null
|
||||||
|
: new Date().toISOString()
|
||||||
|
|
||||||
|
const updatedTasks = tasks.map(task =>
|
||||||
|
task.id === taskId ? updatedTask : task,
|
||||||
|
)
|
||||||
|
CompleteSubTask(
|
||||||
|
taskId,
|
||||||
|
Number(choreId),
|
||||||
|
updatedTask.completedAt ? new Date().toISOString() : null,
|
||||||
|
).then(res => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.log('Error updating task')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTasks(updatedTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = taskId => {
|
||||||
|
setTasks(tasks.filter(task => task.id !== taskId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (!newTask.trim()) return
|
||||||
|
setTasks([
|
||||||
|
...tasks,
|
||||||
|
{
|
||||||
|
name: newTask,
|
||||||
|
completedAt: null,
|
||||||
|
orderId: tasks.length,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setNewTask('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragEnd = event => {
|
||||||
|
const { active, over } = event
|
||||||
|
if (active.id !== over.id) {
|
||||||
|
setTasks(items => {
|
||||||
|
const oldIndex = items.findIndex(item => item.id === active.id)
|
||||||
|
const newIndex = items.findIndex(item => item.id === over.id)
|
||||||
|
const reorderedItems = arrayMove(items, oldIndex, newIndex)
|
||||||
|
return reorderedItems.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
orderId: index,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleAdd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tasks by orderId before rendering
|
||||||
|
const sortedTasks = [...tasks].sort((a, b) => a.orderId - b.orderId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
items={sortedTasks}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<List sx={{ padding: 0 }}>
|
||||||
|
{sortedTasks.map((task, index) => (
|
||||||
|
<SortableItem
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
index={index}
|
||||||
|
handleToggle={handleToggle}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
editMode={editMode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{editMode && (
|
||||||
|
<ListItem sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Input
|
||||||
|
placeholder='Add new...'
|
||||||
|
value={newTask}
|
||||||
|
onChange={e => setNewTask(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={handleAdd}>
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubTasks
|
Loading…
Add table
Reference in a new issue