feat: implement ActivitiesCard component to display recent activities with enhanced status and time display
Support the new Status and Performed By
This commit is contained in:
411
src/views/Chores/ActivitesCard.jsx
Normal file
411
src/views/Chores/ActivitesCard.jsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import {
|
||||
CheckCircle,
|
||||
EventNote,
|
||||
Notes,
|
||||
Person,
|
||||
Refresh,
|
||||
Toll,
|
||||
WatchLater,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Chip,
|
||||
Divider,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemDecorator,
|
||||
Sheet,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||
|
||||
const ActivityItem = ({ activity, members }) => {
|
||||
// Find the member who completed the activity
|
||||
const completedByMember = members?.find(
|
||||
member => member.userId === activity.completedBy,
|
||||
)
|
||||
|
||||
const getTimeDisplay = completedAt => {
|
||||
const now = moment()
|
||||
const completed = moment(completedAt)
|
||||
const diffInHours = now.diff(completed, 'hours')
|
||||
const diffInDays = now.diff(completed, 'days')
|
||||
|
||||
if (diffInHours < 1) {
|
||||
return 'Just now'
|
||||
} else if (diffInHours < 24) {
|
||||
return `${diffInHours}h ago`
|
||||
} else if (diffInDays < 7) {
|
||||
return `${diffInDays}d ago`
|
||||
} else {
|
||||
return completed.format('MMM DD')
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusInfo = activity => {
|
||||
if (!activity.dueDate) {
|
||||
return {
|
||||
color: 'neutral',
|
||||
text: 'Completed',
|
||||
icon: <CheckCircle />,
|
||||
}
|
||||
}
|
||||
|
||||
const wasOnTime = moment(activity.completedAt).isSameOrBefore(
|
||||
moment(activity.dueDate),
|
||||
)
|
||||
|
||||
if (wasOnTime) {
|
||||
return {
|
||||
color: 'success',
|
||||
text: 'On Time',
|
||||
icon: <CheckCircle />,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
color: 'warning',
|
||||
text: 'Late',
|
||||
icon: <WatchLater />,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem sx={{ alignItems: 'flex-start', py: 0.5 }}>
|
||||
<ListItemDecorator sx={{ mt: 0.5 }}>
|
||||
<Avatar
|
||||
size='sm'
|
||||
src={resolvePhotoURL(completedByMember?.image)}
|
||||
sx={{ width: 32, height: 32 }}
|
||||
>
|
||||
{completedByMember?.displayName?.charAt(0) ||
|
||||
completedByMember?.name?.charAt(0) || <Person />}
|
||||
</Avatar>
|
||||
</ListItemDecorator>
|
||||
|
||||
<ListItemContent sx={{ flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{/* Activity header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography level='title-sm' sx={{ flex: 1 }}>
|
||||
{activity.choreName}
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='text.secondary'>
|
||||
{getTimeDisplay(activity.completedAt)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Who completed it */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{/* Status chip */}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={getStatusInfo(activity).color}
|
||||
startDecorator={getStatusInfo(activity).icon}
|
||||
>
|
||||
{getStatusInfo(activity).text}
|
||||
</Chip>
|
||||
<Typography level='body-xs' color='text.secondary' sx={{ ml: 0 }}>
|
||||
by{' '}
|
||||
{completedByMember?.displayName ||
|
||||
completedByMember?.name ||
|
||||
'Unknown'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Status, Points, and Notes */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 0.5,
|
||||
mt: 0.5,
|
||||
ml: 2.5,
|
||||
}}
|
||||
>
|
||||
{/* Points chip */}
|
||||
{activity.points && activity.points > 0 && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='success'
|
||||
startDecorator={<Toll />}
|
||||
>
|
||||
{activity.points} pts
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Notes */}
|
||||
{activity.notes && (
|
||||
<Box sx={{ mt: 0.5, ml: 2.5 }}>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 0.5,
|
||||
fontStyle: 'italic',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
<Notes sx={{ fontSize: 14, mt: 0.1 }} />
|
||||
{activity.notes}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
const groupActivitiesByDate = activities => {
|
||||
const groups = {}
|
||||
|
||||
activities.forEach(activity => {
|
||||
const date = moment(activity.completedAt).format('YYYY-MM-DD')
|
||||
if (!groups[date]) {
|
||||
groups[date] = []
|
||||
}
|
||||
groups[date].push(activity)
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
// Use hooks to fetch data
|
||||
const {
|
||||
data: choresData,
|
||||
isLoading: isChoresLoading,
|
||||
refetch: refetchChores,
|
||||
} = useChores(true) // Include archived chores
|
||||
|
||||
const {
|
||||
data: choreHistory,
|
||||
isLoading: isChoresHistoryLoading,
|
||||
refetch: refetchHistory,
|
||||
} = useChoresHistory(10, true) // Limit to 10 items, include members
|
||||
|
||||
const {
|
||||
data: circleMembersData,
|
||||
isLoading: isCircleMembersLoading,
|
||||
refetch: refetchMembers,
|
||||
} = useCircleMembers()
|
||||
|
||||
// Extract data from responses
|
||||
const chores = choresData?.res || []
|
||||
const members = circleMembersData?.res || []
|
||||
|
||||
// Refresh function to refetch all data
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([refetchChores(), refetchHistory(), refetchMembers()])
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if (isChoresLoading || isChoresHistoryLoading || isCircleMembersLoading) {
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
minHeight: 300,
|
||||
width: '315px',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<EventNote />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 200,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Loading activities...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
// Enrich history with chore names
|
||||
const enrichedHistory =
|
||||
choreHistory?.map(history => {
|
||||
const chore = chores?.find(c => c.id === history.choreId)
|
||||
return {
|
||||
...history,
|
||||
choreName: chore?.name || 'Unknown Chore',
|
||||
}
|
||||
}) || []
|
||||
|
||||
// Sort by completion date (most recent first)
|
||||
const sortedHistory = enrichedHistory
|
||||
.sort(
|
||||
(a, b) =>
|
||||
moment(b.completedAt).valueOf() - moment(a.completedAt).valueOf(),
|
||||
)
|
||||
.slice(0, 10) // Show only latest 10 activities
|
||||
|
||||
const groupedActivities = groupActivitiesByDate(sortedHistory)
|
||||
|
||||
if (!sortedHistory.length) {
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
// width: '290px',
|
||||
minHeight: 300,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<EventNote color='primary' />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 200,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
<EventNote sx={{ fontSize: 48, opacity: 0.3, mb: 1 }} />
|
||||
<Typography level='body-sm'>No recent activities</Typography>
|
||||
</Box>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: 'sm',
|
||||
borderRadius: 20,
|
||||
width: '310px',
|
||||
minHeight: 300,
|
||||
maxHeight: 400,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<EventNote color='primary' />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip size='sm' variant='soft' color='neutral'>
|
||||
{sortedHistory.length}
|
||||
</Chip>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={handleRefresh}
|
||||
sx={{ minHeight: 24, minWidth: 24 }}
|
||||
>
|
||||
<Refresh sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Scrollable activity list */}
|
||||
<Box sx={{ maxHeight: 280, overflowY: 'auto', flex: 1 }}>
|
||||
{Object.entries(groupedActivities).map(([date, activities]) => {
|
||||
const isToday = moment(date).isSame(moment(), 'day')
|
||||
const isYesterday = moment(date).isSame(
|
||||
moment().subtract(1, 'day'),
|
||||
'day',
|
||||
)
|
||||
|
||||
let dateLabel
|
||||
if (isToday) {
|
||||
dateLabel = 'Today'
|
||||
} else if (isYesterday) {
|
||||
dateLabel = 'Yesterday'
|
||||
} else {
|
||||
dateLabel = moment(date).format('MMM DD')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={date} sx={{ mb: 1 }}>
|
||||
{/* Date separator */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 1, px: 1 }}>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
px: 1,
|
||||
pr: 1,
|
||||
mt: -1,
|
||||
color: 'text.secondary',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{dateLabel}
|
||||
</Typography>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
</Box>
|
||||
|
||||
{/* Activities for this date */}
|
||||
<List sx={{ py: 0 }}>
|
||||
{activities.map(activity => (
|
||||
<ActivityItem
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
members={members}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivitiesCard
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import {
|
||||
DeleteChoreHistory,
|
||||
@@ -69,9 +69,9 @@ const ChoreHistory = () => {
|
||||
|
||||
const averageDelay =
|
||||
histories.reduce((acc, chore) => {
|
||||
if (chore.dueDate && chore.completedAt) {
|
||||
if (chore.dueDate && chore.performedAt) {
|
||||
// Only consider chores with a due date
|
||||
return acc + moment(chore.completedAt).diff(chore.dueDate, 'hours')
|
||||
return acc + moment(chore.performedAt).diff(chore.dueDate, 'hours')
|
||||
}
|
||||
return acc
|
||||
}, 0) / histories.filter(chore => chore.dueDate).length
|
||||
@@ -79,7 +79,7 @@ const ChoreHistory = () => {
|
||||
const maximumDelay = histories.reduce((acc, chore) => {
|
||||
if (chore.dueDate) {
|
||||
// Only consider chores with a due date
|
||||
const delay = moment(chore.completedAt).diff(chore.dueDate, 'hours')
|
||||
const delay = moment(chore.performedAt).diff(chore.dueDate, 'hours')
|
||||
return delay > acc ? delay : acc
|
||||
}
|
||||
return acc
|
||||
@@ -242,7 +242,7 @@ const ChoreHistory = () => {
|
||||
},
|
||||
onSave: updated => {
|
||||
UpdateChoreHistory(choreId, editHistory.id, {
|
||||
completedAt: updated.completedAt,
|
||||
performedAt: updated.performedAt,
|
||||
dueDate: updated.dueDate,
|
||||
notes: updated.notes,
|
||||
}).then(res => {
|
||||
|
||||
@@ -18,15 +18,15 @@ export const getCompletedChip = historyEntry => {
|
||||
// if completed few hours +-6 hours
|
||||
if (
|
||||
historyEntry.dueDate &&
|
||||
historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
|
||||
historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
|
||||
historyEntry.performedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
|
||||
historyEntry.performedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
|
||||
) {
|
||||
text = 'On Time'
|
||||
color = 'success'
|
||||
icon = <Check />
|
||||
} else if (
|
||||
historyEntry.dueDate &&
|
||||
historyEntry.completedAt < historyEntry.dueDate
|
||||
historyEntry.performedAt < historyEntry.dueDate
|
||||
) {
|
||||
text = 'On Time'
|
||||
color = 'success'
|
||||
@@ -36,7 +36,7 @@ export const getCompletedChip = historyEntry => {
|
||||
// if completed after due date then it's late
|
||||
else if (
|
||||
historyEntry.dueDate &&
|
||||
historyEntry.completedAt > historyEntry.dueDate
|
||||
historyEntry.performedAt > historyEntry.dueDate
|
||||
) {
|
||||
text = 'Late'
|
||||
color = 'warning'
|
||||
@@ -104,8 +104,8 @@ const HistoryCard = ({
|
||||
}}
|
||||
>
|
||||
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||||
{historyEntry.completedAt
|
||||
? moment(historyEntry.completedAt).format(
|
||||
{historyEntry.performedAt
|
||||
? moment(historyEntry.performedAt).format(
|
||||
'ddd MM/DD/yyyy HH:mm',
|
||||
)
|
||||
: 'Skipped'}
|
||||
@@ -150,11 +150,11 @@ const HistoryCard = ({
|
||||
<ListDivider component='li'>
|
||||
{/* time between two completion: */}
|
||||
{index < allHistory.length - 1 &&
|
||||
allHistory[index + 1].completedAt && (
|
||||
allHistory[index + 1].performedAt && (
|
||||
<Typography level='body3' color='text.tertiary'>
|
||||
{formatTimeDifference(
|
||||
historyEntry.completedAt,
|
||||
allHistory[index + 1].completedAt,
|
||||
historyEntry.performedAt,
|
||||
allHistory[index + 1].performedAt,
|
||||
)}{' '}
|
||||
before
|
||||
</Typography>
|
||||
|
||||
@@ -14,7 +14,7 @@ import ConfirmationModal from './Inputs/ConfirmationModal'
|
||||
function EditHistoryModal({ config, historyRecord }) {
|
||||
useEffect(() => {
|
||||
setCompletedDate(
|
||||
moment(historyRecord.completedAt).format('YYYY-MM-DDTHH:mm'),
|
||||
moment(historyRecord.performedAt).format('YYYY-MM-DDTHH:mm'),
|
||||
)
|
||||
setDueDate(moment(historyRecord.dueDate).format('YYYY-MM-DDTHH:mm'))
|
||||
setNotes(historyRecord.notes)
|
||||
@@ -76,7 +76,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
onClick={() =>
|
||||
config.onSave({
|
||||
id: historyRecord.id,
|
||||
completedAt: moment(completedDate).toISOString(),
|
||||
performedAt: moment(completedDate).toISOString(),
|
||||
dueDate: moment(dueDate).toISOString(),
|
||||
notes,
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ const groupByDate = history => {
|
||||
const aggregated = {}
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const item = history[i]
|
||||
const date = new Date(item.completedAt).toLocaleDateString()
|
||||
const date = new Date(item.performedAt).toLocaleDateString()
|
||||
if (!aggregated[date]) {
|
||||
aggregated[date] = []
|
||||
}
|
||||
@@ -114,7 +114,7 @@ const ChoreHistoryTimeline = ({ history }) => {
|
||||
<>
|
||||
<ChoreHistoryItem
|
||||
key={record.id}
|
||||
time={new Date(record.completedAt).toLocaleTimeString([], {
|
||||
time={new Date(record.performedAt).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
@@ -289,9 +289,9 @@ const UserActivites = () => {
|
||||
|
||||
const generateHistoryPieChartData = history => {
|
||||
const totalCompleted =
|
||||
history.filter(item => item.dueDate > item.completedAt).length || 0
|
||||
history.filter(item => item.dueDate > item.performedAt).length || 0
|
||||
const totalLate =
|
||||
history.filter(item => item.dueDate < item.completedAt).length || 0
|
||||
history.filter(item => item.dueDate < item.performedAt).length || 0
|
||||
const totalNoDueDate = history.filter(item => !item.dueDate).length || 0
|
||||
|
||||
return [
|
||||
|
||||
@@ -107,7 +107,7 @@ const UserPoints = () => {
|
||||
})
|
||||
}
|
||||
history.forEach(chore => {
|
||||
const dayName = new Date(chore.completedAt).toLocaleString('en-US', {
|
||||
const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
|
||||
weekday: 'short',
|
||||
})
|
||||
|
||||
@@ -136,7 +136,7 @@ const UserPoints = () => {
|
||||
})
|
||||
}
|
||||
history.forEach(chore => {
|
||||
const dayName = new Date(chore.completedAt).toLocaleString('en-US', {
|
||||
const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
@@ -167,7 +167,7 @@ const UserPoints = () => {
|
||||
})
|
||||
}
|
||||
history.forEach(chore => {
|
||||
const monthName = new Date(chore.completedAt).toLocaleString('en-US', {
|
||||
const monthName = new Date(chore.performedAt).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
})
|
||||
|
||||
@@ -198,7 +198,7 @@ const UserPoints = () => {
|
||||
})
|
||||
}
|
||||
history.forEach(chore => {
|
||||
const yearName = new Date(chore.completedAt).toLocaleString('en-US', {
|
||||
const yearName = new Date(chore.performedAt).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useColorScheme } from '@mui/joy'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AutocompleteDropdown from './AutocompleteDropdown'
|
||||
import AutocompleteDropdown from '../TestView/AutocompleteDropdown'
|
||||
import './SmartTaskTitleInput.css'
|
||||
const renderHighlightedText = (text, cursorPosition) => {
|
||||
const parts = []
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DndContext, closestCenter } from '@dnd-kit/core'
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
@@ -24,7 +30,7 @@ import {
|
||||
ListItem,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||
|
||||
function SortableItem({
|
||||
@@ -39,7 +45,17 @@ function SortableItem({
|
||||
editMode,
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: task.id })
|
||||
useSortable({
|
||||
id: task.id,
|
||||
// Add touch sensor options for better mobile scrolling
|
||||
options: {
|
||||
activationConstraint: {
|
||||
// Require a small movement before activating drag to allow scrolling
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedText, setEditedText] = useState(task.name)
|
||||
@@ -58,7 +74,8 @@ function SortableItem({
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
touchAction: 'none',
|
||||
// Enable default touch behavior for scrolling
|
||||
touchAction: 'auto',
|
||||
paddingLeft: `${level * 24}px`,
|
||||
}
|
||||
|
||||
@@ -102,7 +119,15 @@ function SortableItem({
|
||||
<>
|
||||
<ListItem ref={setNodeRef} style={style} {...attributes}>
|
||||
{editMode && (
|
||||
<IconButton {...listeners} {...attributes} size='sm'>
|
||||
<IconButton
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
size='sm'
|
||||
// Add data attribute for selective activation
|
||||
data-drag-handle='true'
|
||||
// Only restrict touch actions on the drag handle
|
||||
sx={{ touchAction: 'none' }}
|
||||
>
|
||||
<DragIndicator />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -119,7 +144,7 @@ function SortableItem({
|
||||
)}
|
||||
|
||||
{!hasChildren && level > 0 && (
|
||||
<Box sx={{ width: 28 }} /> // Spacer for alignment not sure of better way for now it's good
|
||||
<Box sx={{ width: 28 }} /> // Spacer for alignment
|
||||
)}
|
||||
|
||||
<Box
|
||||
@@ -269,6 +294,17 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
|
||||
|
||||
const topLevelTasks = tasks.filter(task => task.parentId === null)
|
||||
|
||||
// Create sensors for touch handling
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
// Configure for better mobile scrolling
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 8,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const handleToggle = taskId => {
|
||||
const updatedTask = tasks.find(task => task.id === taskId)
|
||||
const newCompletedAt = updatedTask.completedAt
|
||||
@@ -405,9 +441,21 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<SortableContext items={tasks} strategy={verticalListSortingStrategy}>
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List
|
||||
sx={{
|
||||
padding: 0,
|
||||
// Improve scrolling behavior on mobile
|
||||
maxHeight: 'inherit',
|
||||
overflow: 'visible',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{topLevelTasks
|
||||
.sort((a, b) => a.orderId - b.orderId)
|
||||
.map((task, index) => (
|
||||
|
||||
Reference in New Issue
Block a user