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",
|
||||
"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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
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