Bump version to 0.1.95 and add dnd-kit dependencies; implement CompleteSubTask function and enhance chore sorting logic

This commit is contained in:
Mo Tarbin 2025-02-25 23:39:59 -05:00
parent e359b1c0a4
commit bbea27d380
9 changed files with 511 additions and 135 deletions

59
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "donetick",
"version": "0.1.91",
"version": "0.1.94",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "donetick",
"version": "0.1.91",
"version": "0.1.94",
"dependencies": {
"@capacitor/android": "^6.1.1",
"@capacitor/app": "^6.0.0",
@ -18,6 +18,8 @@
"@capacitor/preferences": "^6.0.1",
"@capacitor/push-notifications": "^6.0.1",
"@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/styled": "^11.11.0",
"@mui/icons-material": "^5.15.2",
@ -1804,6 +1806,59 @@
"@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": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",

View file

@ -1,7 +1,7 @@
{
"name": "donetick",
"private": true,
"version": "0.1.94",
"version": "0.1.95",
"type": "module",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
@ -32,6 +32,8 @@
"@capacitor/preferences": "^6.0.1",
"@capacitor/push-notifications": "^6.0.1",
"@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/styled": "^11.11.0",
"@mui/icons-material": "^5.15.2",

View file

@ -1,25 +1,11 @@
import moment from 'moment'
import { TASK_COLOR } from './Colors.jsx'
const priorityOrder = [1, 2, 3, 4, 0]
export const ChoresGrouper = (groupBy, chores) => {
// sort by priority then due date:
chores.sort((a, b) => {
// 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)
})
chores.sort(ChoreSorter)
var groups = []
switch (groupBy) {
case 'due_date':
@ -159,7 +145,25 @@ export const ChoresGrouper = (groupBy, chores) => {
}
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 => {
return (
chore.completionWindow &&

View file

@ -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 => {
return Fetch(`/chores/${id}/skip`, {
method: 'POST',
@ -476,6 +485,7 @@ export {
ArchiveChore,
CancelSubscription,
ChangePassword,
CompleteSubTask,
CreateChore,
CreateLabel,
CreateLongLiveToken,

View file

@ -7,16 +7,10 @@ import {
const Priorities = [
{
name: 'P4',
value: 4,
icon: <HorizontalRule />,
color: '',
},
{
name: 'P3 ',
value: 3,
icon: <KeyboardControlKey />,
color: '',
name: 'P1',
value: 1,
icon: <PriorityHigh />,
color: 'danger',
},
{
name: 'P2',
@ -25,10 +19,16 @@ const Priorities = [
color: 'warning',
},
{
name: 'P1',
value: 1,
icon: <PriorityHigh />,
color: 'danger',
name: 'P3 ',
value: 3,
icon: <KeyboardControlKey />,
color: '',
},
{
name: 'P4',
value: 4,
icon: <HorizontalRule />,
color: '',
},
]

View file

@ -9,7 +9,6 @@ import {
Divider,
FormControl,
FormHelperText,
FormLabel,
Input,
List,
ListItem,
@ -40,6 +39,8 @@ import {
SaveChore,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import Priorities from '../../utils/Priorities.jsx'
import SubTasks from '../components/SubTask.jsx'
import { useLabels } from '../Labels/LabelQueries'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import LabelModal from '../Modals/Inputs/LabelModal'
@ -76,7 +77,9 @@ const ChoreEdit = () => {
const [frequencyMetadata, setFrequencyMetadata] = useState({})
const [labels, setLabels] = useState([])
const [labelsV2, setLabelsV2] = useState([])
const [priority, setPriority] = useState(0)
const [points, setPoints] = useState(-1)
const [subTasks, setSubTasks] = useState(null)
const [completionWindow, setCompletionWindow] = useState(-1)
const [allUserThings, setAllUserThings] = useState([])
const [thingTrigger, setThingTrigger] = useState(null)
@ -201,10 +204,12 @@ const ChoreEdit = () => {
notification: isNotificable,
labels: labels.map(l => l.name),
labelsV2: labelsV2,
subTasks: subTasks,
notificationMetadata: notificationMetadata,
thingTrigger: thingTrigger,
points: points < 0 ? null : points,
completionWindow: completionWindow < 0 ? null : completionWindow,
priority: priority,
}
let SaveFunction = CreateChore
if (choreId > 0) {
@ -265,6 +270,8 @@ const ChoreEdit = () => {
)
setLabelsV2(data.res.labelsV2)
setSubTasks(data.res.subTasks)
setPriority(data.res.priority)
setAssignStrategy(
data.res.assignStrategy
? data.res.assignStrategy
@ -382,7 +389,7 @@ const ChoreEdit = () => {
</Typography> */}
<Box>
<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>
<Input value={name} onChange={e => setName(e.target.value)} />
<FormHelperText error>{errors.name}</FormHelperText>
@ -390,8 +397,8 @@ const ChoreEdit = () => {
</Box>
<Box mt={2}>
<FormControl error={errors.description}>
<Typography level='h4'>Details:</Typography>
<Typography level='h5'>What is this chore about?</Typography>
<Typography level='h4'>Additional Details :</Typography>
<Typography level='h5'>What is this task about?</Typography>
<Textarea
value={description}
onChange={e => setDescription(e.target.value)}
@ -401,7 +408,7 @@ const ChoreEdit = () => {
</Box>
<Box mt={2}>
<Typography level='h4'>Assignees :</Typography>
<Typography level='h5'>Who can do this chore?</Typography>
<Typography level='h5'>Who can do this task?</Typography>
<Card>
<List
orientation='horizontal'
@ -590,18 +597,7 @@ const ChoreEdit = () => {
</FormControl>
)}
<FormControl
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>
<FormControl orientation='horizontal'>
<Switch
checked={completionWindow != -1}
onClick={event => {
@ -615,14 +611,18 @@ const ChoreEdit = () => {
color={completionWindow !== -1 ? 'success' : 'neutral'}
variant={completionWindow !== -1 ? 'solid' : 'outlined'}
// endDecorator={points !== -1 ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
sx={{
mr: 2,
}}
/>
<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>
{completionWindow != -1 && (
<Card variant='outlined'>
@ -913,44 +913,89 @@ const ChoreEdit = () => {
</MenuItem>
</Select>
</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}>
<Typography level='h4' gutterBottom>
Others :
</Typography>
<FormControl
orientation='horizontal'
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
<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)
<FormControl sx={{ mt: 1 }}>
<Checkbox
onChange={e => {
if (e.target.checked) {
setSubTasks([])
} else {
setPoints(1)
setSubTasks(null)
}
}}
color={points !== -1 ? 'success' : 'neutral'}
variant={points !== -1 ? 'solid' : 'outlined'}
// endDecorator={points !== -1 ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
overlay
checked={subTasks != null}
value={subTasks != null}
label='Sub Tasks'
/>
<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>
{points != -1 && (

View file

@ -48,6 +48,7 @@ import {
} from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import SubTasks from '../components/SubTask.jsx'
const IconCard = styled('div')({
display: 'flex',
alignItems: 'center',
@ -483,13 +484,33 @@ const ChoreView = () => {
<Typography level='title-md' sx={{ mb: 1 }}>
Previous note:
</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 }}>
{chore.notes || '--'}
</Typography>
</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>
<Card
@ -497,7 +518,6 @@ const ChoreView = () => {
p: 2,
borderRadius: 'md',
boxShadow: 'sm',
mt: 2,
}}
variant='soft'
>
@ -612,7 +632,12 @@ const ChoreView = () => {
fullWidth
size='lg'
onClick={handleTaskCompletion}
disabled={isPendingCompletion || notInCompletionWindow(chore)}
disabled={
isPendingCompletion ||
notInCompletionWindow(chore) ||
(chore.lastCompletedDate !== null &&
chore.frequencyType === 'once')
}
color={isPendingCompletion ? 'danger' : 'success'}
startDecorator={<Check />}
sx={{
@ -642,6 +667,9 @@ const ChoreView = () => {
},
})
}}
disabled={
chore.lastCompletedDate !== null && chore.frequencyType === 'once'
}
startDecorator={<SwitchAccessShortcut />}
sx={{
flex: 1,

View file

@ -3,7 +3,7 @@ import {
CancelRounded,
EditCalendar,
ExpandCircleDown,
FilterAlt,
Grain,
PriorityHigh,
Search,
Sort,
@ -44,7 +44,7 @@ import { useLabels } from '../Labels/LabelQueries'
import ChoreCard from './ChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
import { ChoresGrouper } from '../../utils/Chores'
import { ChoresGrouper, ChoreSorter } from '../../utils/Chores'
import TaskInput from '../components/AddTaskModal'
import {
canScheduleNotification,
@ -66,8 +66,12 @@ const MyChores = () => {
const [taskInputFocus, setTaskInputFocus] = useState(0)
const searchInputRef = useRef()
const [searchInputFocus, setSearchInputFocus] = useState(0)
const [selectedChoreSection, setSelectedChoreSection] = useState('due_date')
const [openChoreSections, setOpenChoreSections] = useState({})
const [selectedChoreSection, setSelectedChoreSection] = useState(
localStorage.getItem('selectedChoreSection') || 'due_date',
)
const [openChoreSections, setOpenChoreSections] = useState(
JSON.parse(localStorage.getItem('openChoreSections')) || {},
)
const [searchTerm, setSearchTerm] = useState('')
const [performers, setPerformers] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
@ -75,33 +79,6 @@ const MyChores = () => {
const Navigate = useNavigate()
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
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(() => {
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
@ -123,7 +100,7 @@ const MyChores = () => {
]).then(data => {
const [choresData, usersData, userProfileData] = data
setUserProfile(userProfileData.res)
choresData.res.sort(choreSorter)
choresData.res.sort(ChoreSorter)
setChores(choresData.res)
setFilteredChores(choresData.res)
setPerformers(usersData.res)
@ -155,17 +132,20 @@ const MyChores = () => {
useEffect(() => {
if (choresData) {
const sortedChores = choresData.res.sort(choreSorter)
const sortedChores = choresData.res.sort(ChoreSorter)
setChores(sortedChores)
setFilteredChores(sortedChores)
const sections = ChoresGrouper('due_date', sortedChores)
const sections = ChoresGrouper(selectedChoreSection, sortedChores)
setChoreSections(sections)
setOpenChoreSections(
Object.keys(sections).reduce((acc, key) => {
acc[key] = true
return acc
}, {}),
)
if (localStorage.getItem('openChoreSections') === null) {
setSelectedChoreSectionWithCache(selectedChoreSection)
setOpenChoreSections(
Object.keys(sections).reduce((acc, key) => {
acc[key] = true
return acc
}, {}),
)
}
}
}, [choresData, choresLoading])
@ -184,12 +164,21 @@ const MyChores = () => {
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
}
}, [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 newChores = chores
newChores.push(newChore)
setChores(newChores)
setFilteredChores(newChores)
setChoreSections(ChoresGrouper('due_date', newChores))
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
setSelectedFilter('All')
}
const handleMenuOutsideClick = event => {
@ -265,7 +254,7 @@ const MyChores = () => {
}
setChores(newChores)
setFilteredChores(newFilteredChores)
setChoreSections(ChoresGrouper('due_date', newChores))
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
switch (event) {
case 'completed':
@ -296,7 +285,7 @@ const MyChores = () => {
)
setChores(newChores)
setFilteredChores(newFilteredChores)
setChoreSections(ChoresGrouper('due_date', newChores))
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
}
const searchOptions = {
@ -443,7 +432,14 @@ const MyChores = () => {
onItemSelect={selected => {
const section = ChoresGrouper(selected.value, chores)
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)
setSelectedFilter('All')
}}
@ -483,7 +479,7 @@ const MyChores = () => {
<Button
onClick={handleFilterMenuOpen}
variant='outlined'
startDecorator={<FilterAlt />}
startDecorator={<Grain />}
color={
selectedFilter &&
FILTERS[selectedFilter] &&
@ -649,9 +645,9 @@ const MyChores = () => {
...openChoreSections,
}
delete newOpenChoreSections[index]
setOpenChoreSections(newOpenChoreSections)
setOpenChoreSectionsWithCache(newOpenChoreSections)
} else {
setOpenChoreSections({
setOpenChoreSectionsWithCache({
...openChoreSections,
[index]: true,
})

View 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