From 1f2d38730da343b8d45ab992c7eb9f86ff22ee2a Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 29 May 2025 01:01:07 -0400 Subject: [PATCH] feat: implement ActivitiesCard component to display recent activities with enhanced status and time display Support the new Status and Performed By --- src/views/Chores/ActivitesCard.jsx | 411 ++++++++++++++++++ src/views/History/ChoreHistory.jsx | 10 +- src/views/History/HistoryCard.jsx | 18 +- src/views/Modals/EditHistoryModal.jsx | 4 +- src/views/User/UserActivities.jsx | 8 +- src/views/User/UserPoints.jsx | 8 +- .../SmartTaskTitleInput.css | 0 .../SmartTaskTitleInput.jsx | 2 +- src/views/components/SubTask.jsx | 64 ++- 9 files changed, 492 insertions(+), 33 deletions(-) create mode 100644 src/views/Chores/ActivitesCard.jsx rename src/views/{TestView => components}/SmartTaskTitleInput.css (100%) rename src/views/{TestView => components}/SmartTaskTitleInput.jsx (99%) diff --git a/src/views/Chores/ActivitesCard.jsx b/src/views/Chores/ActivitesCard.jsx new file mode 100644 index 0000000..dbf1363 --- /dev/null +++ b/src/views/Chores/ActivitesCard.jsx @@ -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: , + } + } + + const wasOnTime = moment(activity.completedAt).isSameOrBefore( + moment(activity.dueDate), + ) + + if (wasOnTime) { + return { + color: 'success', + text: 'On Time', + icon: , + } + } else { + return { + color: 'warning', + text: 'Late', + icon: , + } + } + } + + return ( + + + + {completedByMember?.displayName?.charAt(0) || + completedByMember?.name?.charAt(0) || } + + + + + + {/* Activity header */} + + + {activity.choreName} + + + {getTimeDisplay(activity.completedAt)} + + + + {/* Who completed it */} + + {/* Status chip */} + + {getStatusInfo(activity).text} + + + by{' '} + {completedByMember?.displayName || + completedByMember?.name || + 'Unknown'} + + + + {/* Status, Points, and Notes */} + + {/* Points chip */} + {activity.points && activity.points > 0 && ( + } + > + {activity.points} pts + + )} + + + {/* Notes */} + {activity.notes && ( + + + + {activity.notes} + + + )} + + + + ) +} + +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 ( + + + + {title} + + + + Loading activities... + + + + ) + } + + // 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 ( + + + + {title} + + + + No recent activities + + + ) + } + + return ( + + {/* Header */} + + + + + {title} + + + + {sortedHistory.length} + + + + + + + + + {/* Scrollable activity list */} + + {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 ( + + {/* Date separator */} + + + + {dateLabel} + + + + + {/* Activities for this date */} + + {activities.map(activity => ( + + ))} + + + ) + })} + + + ) +} + +export default ActivitiesCard diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index d5b3caf..4f7595d 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -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 => { diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx index 04e999c..5203850 100644 --- a/src/views/History/HistoryCard.jsx +++ b/src/views/History/HistoryCard.jsx @@ -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 = } 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 = ({ }} > - {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 = ({ {/* time between two completion: */} {index < allHistory.length - 1 && - allHistory[index + 1].completedAt && ( + allHistory[index + 1].performedAt && ( {formatTimeDifference( - historyEntry.completedAt, - allHistory[index + 1].completedAt, + historyEntry.performedAt, + allHistory[index + 1].performedAt, )}{' '} before diff --git a/src/views/Modals/EditHistoryModal.jsx b/src/views/Modals/EditHistoryModal.jsx index 1baab49..2450759 100644 --- a/src/views/Modals/EditHistoryModal.jsx +++ b/src/views/Modals/EditHistoryModal.jsx @@ -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, }) diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx index 46a3564..d10ee0b 100644 --- a/src/views/User/UserActivities.jsx +++ b/src/views/User/UserActivities.jsx @@ -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 }) => { <> { 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 [ diff --git a/src/views/User/UserPoints.jsx b/src/views/User/UserPoints.jsx index effe8d4..8746f93 100644 --- a/src/views/User/UserPoints.jsx +++ b/src/views/User/UserPoints.jsx @@ -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', }) diff --git a/src/views/TestView/SmartTaskTitleInput.css b/src/views/components/SmartTaskTitleInput.css similarity index 100% rename from src/views/TestView/SmartTaskTitleInput.css rename to src/views/components/SmartTaskTitleInput.css diff --git a/src/views/TestView/SmartTaskTitleInput.jsx b/src/views/components/SmartTaskTitleInput.jsx similarity index 99% rename from src/views/TestView/SmartTaskTitleInput.jsx rename to src/views/components/SmartTaskTitleInput.jsx index c9fd015..1f1563b 100644 --- a/src/views/TestView/SmartTaskTitleInput.jsx +++ b/src/views/components/SmartTaskTitleInput.jsx @@ -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 = [] diff --git a/src/views/components/SubTask.jsx b/src/views/components/SubTask.jsx index 676dbc2..ae214bf 100644 --- a/src/views/components/SubTask.jsx +++ b/src/views/components/SubTask.jsx @@ -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({ <> {editMode && ( - + )} @@ -119,7 +144,7 @@ function SortableItem({ )} {!hasChildren && level > 0 && ( - // Spacer for alignment not sure of better way for now it's good + // Spacer for alignment )} { 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 ( <> - + - + {topLevelTasks .sort((a, b) => a.orderId - b.orderId) .map((task, index) => (