From 4f444bd3c9a4d63e2c7458a2dc59976bc47b9c79 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 20 Jul 2025 10:41:14 -0400 Subject: [PATCH 1/2] Feature: implement useThingHistory hook and refactor ThingsHistory component for improved data fetching and rendering --- src/queries/ThingQueries.jsx | 29 ++ src/views/Authorization/Authenticating.jsx | 2 +- src/views/Things/ThingsHistory.jsx | 70 ++-- src/views/User/UserActivities.jsx | 375 +++++++++++++++------ 4 files changed, 344 insertions(+), 132 deletions(-) create mode 100644 src/queries/ThingQueries.jsx diff --git a/src/queries/ThingQueries.jsx b/src/queries/ThingQueries.jsx new file mode 100644 index 0000000..d655795 --- /dev/null +++ b/src/queries/ThingQueries.jsx @@ -0,0 +1,29 @@ +import { useInfiniteQuery } from '@tanstack/react-query' +import { GetThingHistory } from '../utils/Fetcher' + +export const useThingHistory = (thingId, limit = 10) => { + return useInfiniteQuery({ + queryKey: ['thingHistory', thingId], + queryFn: async ({ pageParam = 0 }) => { + const response = await GetThingHistory(thingId, pageParam) + if (!response.ok) { + throw new Error('Failed to fetch thing history') + } + const data = await response.json() + return data + }, + getNextPageParam: (lastPage, allPages) => { + // If the last page has fewer items than the limit, there are no more pages + if (lastPage.res.length < limit) { + return undefined + } + // Calculate the offset for the next page + const totalItems = allPages.reduce( + (acc, page) => acc + page.res.length, + 0, + ) + return totalItems + }, + enabled: !!thingId, // Only run query if thingId exists + }) +} diff --git a/src/views/Authorization/Authenticating.jsx b/src/views/Authorization/Authenticating.jsx index b90215c..36ce15c 100644 --- a/src/views/Authorization/Authenticating.jsx +++ b/src/views/Authorization/Authenticating.jsx @@ -29,7 +29,7 @@ const AuthenticationLoading = () => { const getUserProfileAndNavigateToHome = () => { GetUserProfile().then(data => { data.json().then(data => { - refetchUserProfile.then(() => { + refetchUserProfile().then(() => { // check if redirect url is set in cookie: const redirectUrl = Cookies.get('ca_redirect') if (redirectUrl) { diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx index fe8b97a..12fb591 100644 --- a/src/views/Things/ThingsHistory.jsx +++ b/src/views/Things/ThingsHistory.jsx @@ -13,7 +13,6 @@ import { Typography, } from '@mui/joy' import moment from 'moment' -import { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { Line, @@ -23,40 +22,27 @@ import { XAxis, YAxis, } from 'recharts' -import { GetThingHistory } from '../../utils/Fetcher' +import { useTheme } from '@mui/joy/styles' +import { useThingHistory } from '../../queries/ThingQueries' import LoadingComponent from '../components/Loading' const ThingsHistory = () => { const { id } = useParams() - const [thingsHistory, setThingsHistory] = useState([]) - const [noMoreHistory, setNoMoreHistory] = useState(false) - const [errLoading, setErrLoading] = useState(false) - useEffect(() => { - GetThingHistory(id, 0, 10).then(resp => { - if (resp.ok) { - resp.json().then(data => { - setThingsHistory(data.res) - if (data.res.length < 10) { - setNoMoreHistory(true) - } - }) - } else { - setErrLoading(true) - } - }) - }, [id]) + const theme = useTheme() + const { + data, + error, + isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useThingHistory(id) + + // Flatten all pages of history data + const thingsHistory = data?.pages.flatMap(page => page.res) || [] const handleLoadMore = () => { - GetThingHistory(id, thingsHistory.length).then(resp => { - if (resp.ok) { - resp.json().then(data => { - setThingsHistory([...thingsHistory, ...data.res]) - if (data.res.length < 10) { - setNoMoreHistory(true) - } - }) - } - }) + fetchNextPage() } const formatTimeDifference = (startDate, endDate) => { @@ -79,11 +65,11 @@ const ThingsHistory = () => { return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}` } // if loading show loading spinner: - if (thingsHistory.length === 0) { + if (isLoading) { return } - if (errLoading || !thingsHistory || thingsHistory.length === 0) { + if (error || !thingsHistory || thingsHistory.length === 0) { return ( { @@ -305,9 +299,13 @@ const ThingsHistory = () => { fullWidth color='primary' onClick={handleLoadMore} - disabled={noMoreHistory} + disabled={!hasNextPage || isFetchingNextPage} > - {noMoreHistory ? 'No more history' : 'Load more'} + {isFetchingNextPage + ? 'Loading...' + : !hasNextPage + ? 'No more history' + : 'Load more'} diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx index 598f2f7..0d4d16c 100644 --- a/src/views/User/UserActivities.jsx +++ b/src/views/User/UserActivities.jsx @@ -1,7 +1,7 @@ import CancelIcon from '@mui/icons-material/Cancel' import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CircleIcon from '@mui/icons-material/Circle' -import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts' +import { Cell, Pie, PieChart, Tooltip } from 'recharts' import { EventBusy, Group, Toll } from '@mui/icons-material' import { @@ -131,44 +131,202 @@ const ChoreHistoryTimeline = ({ history }) => { ) } -const renderPieChart = (data, size, isPrimary, chartType = null) => ( - - - {data.map((entry, index) => ( - - ))} - - {isPrimary && ( - { - if (chartType === 'tasksTime' && props.payload.count) { - return [`${value}h (${props.payload.count} times)`, name] - } - return [`${value}`, name] +const renderPieChart = (data, size, isPrimary, chartType = null) => { + // Filter out items with zero or negative values + const validData = data.filter(item => item.value > 0) + + if (validData.length === 0) { + return ( + - )} - {isPrimary && ( - `${label}: ${value.payload.value}`} - /> - )} - -) + > + + No data available + + + ) + } + + // For primary charts, render chart and legend separately to control layout better + if (isPrimary) { + const chartSize = Math.min(size - 20, 220) // Reserve space and limit max size + + return ( + + {/* Chart Container */} + + + 1 ? 2 : 0} + cornerRadius={3} + minAngle={5} + > + {validData.map((entry, index) => ( + + ))} + + { + if (chartType === 'tasksTime' && props.payload.count) { + return [`${value}h (${props.payload.count} times)`, name] + } + return [`${value}`, name] + }} + /> + + + + {/* Scrollable Legend Container */} + + + {validData.map((entry, index) => ( + + } + > + + {entry.label}: {entry.value} + {chartType === 'tasksTime' && entry.count + ? ` (${entry.count}x)` + : ''} + {chartType === 'labelsDuration' || chartType === 'tasksTime' + ? 'h' + : ''} + + + ))} + + + + ) + } + + // For small preview charts, keep it simple without legend + return ( + + 1 ? 1 : 0} + cornerRadius={2} + > + {validData.map((entry, index) => ( + + ))} + + + ) +} const USER_FILTER = (history, userId) => { if (userId === undefined || userId === 'all') return true @@ -185,9 +343,6 @@ const UserActivites = () => { const [historyPieChartData, setHistoryPieChartData] = React.useState([]) const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([]) - const [choresAssignedChartData, setChoresAssignedChartData] = React.useState( - [], - ) const [choresPriorityChartData, setChoresPriorityChartData] = React.useState( [], ) @@ -247,6 +402,13 @@ const UserActivites = () => { // Generate tasks time chart data setTasksTimeChartData(generateTasksTimeChartData(filteredHistory)) + } else { + // Reset data when loading or no data + setEnrichedHistory([]) + setSelectedHistory([]) + setHistoryPieChartData([]) + setChoresLabelsDurationChartData([]) + setTasksTimeChartData([]) } }, [ isChoresHistoryLoading, @@ -264,35 +426,6 @@ const UserActivites = () => { ? choresData.res : choresData.res.filter(chore => chore.assignedTo === selectedUser) - const generateChoreAssignedChartData = chores => { - var assignedToMe = 0 - var assignedToOthers = 0 - chores.forEach(chore => { - if (chore.assignedTo === userProfile?.id) { - assignedToMe++ - } else assignedToOthers++ - }) - - const group = [] - if (assignedToMe > 0) { - group.push({ - label: `Assigned to me`, - value: assignedToMe, - color: TASK_COLOR.ASSIGNED_TO_ME, - id: 1, - }) - } - if (assignedToOthers > 0) { - group.push({ - label: `Assigned to others`, - value: assignedToOthers, - color: TASK_COLOR.ASSIGNED_TO_OTHERS, - id: 2, - }) - } - return group - } - const generateChorePriorityPieChartData = chores => { const groups = ChoresGrouper('priority', chores, null) return groups @@ -400,7 +533,6 @@ const UserActivites = () => { const choreDuePieChartData = generateChoreDuePieChartData(filteredChores) setChoreDuePieChartData(choreDuePieChartData) - setChoresAssignedChartData(generateChoreAssignedChartData(filteredChores)) setChoresPriorityChartData( generateChorePriorityPieChartData(filteredChores), ) @@ -412,6 +544,10 @@ const UserActivites = () => { }, [isChoresLoading, choresData, userProfile?.id, circleUsers, selectedUser]) const generateChoreLabelsWithDurationChartData = (chores, history) => { + if (!chores || !history || chores.length === 0 || history.length === 0) { + return [] + } + const labelDurations = {} let unlabeledDuration = 0 @@ -467,6 +603,10 @@ const UserActivites = () => { } const generateTasksTimeChartData = history => { + if (!history || history.length === 0) { + return [] + } + const taskDurations = {} const colorValues = Object.values(COLORS) @@ -504,6 +644,10 @@ const UserActivites = () => { } const generateChoreDuePieChartData = chores => { + if (!chores || chores.length === 0) { + return [] + } + const groups = ChoresGrouper('due_date', chores, null) return groups .map(group => { @@ -518,44 +662,58 @@ const UserActivites = () => { } const generateHistoryPieChartData = history => { + if (!history || history.length === 0) { + return [] + } + const totalCompleted = history.filter(item => item.dueDate > item.performedAt).length || 0 const totalLate = history.filter(item => item.dueDate < item.performedAt).length || 0 const totalNoDueDate = history.filter(item => !item.dueDate).length || 0 - return [ - { + const result = [] + + if (totalCompleted > 0) { + result.push({ label: `On time`, value: totalCompleted, color: TASK_COLOR.COMPLETED, id: 1, - }, - { + }) + } + + if (totalLate > 0) { + result.push({ label: `Late`, value: totalLate, color: TASK_COLOR.LATE, id: 2, - }, - { + }) + } + + if (totalNoDueDate > 0) { + result.push({ label: `Completed`, value: totalNoDueDate, color: TASK_COLOR.ANYTIME, id: 3, - }, - ] + }) + } + + return result } if (isChoresHistoryLoading || isChoresLoading) { return } const chartData = { history: { - data: historyPieChartData, + data: historyPieChartData || [], title: 'Status', description: 'Completed tasks status', }, due: { - data: choreDuePieChartData, + data: choreDuePieChartData || [], title: 'Due Date', description: 'Current tasks due date', }, @@ -565,27 +723,27 @@ const UserActivites = () => { // description: 'Tasks assigned to you vs others', // }, priority: { - data: choresPriorityChartData, + data: choresPriorityChartData || [], title: 'Priority', description: 'Tasks by priority', }, labels: { - data: choresLabelsChartData, + data: choresLabelsChartData || [], title: 'Labels', description: 'Tasks by labels', }, labelsDuration: { - data: choresLabelsDurationChartData, + data: choresLabelsDurationChartData || [], title: 'Labels (time)', description: 'Time spent by labels (hours)', }, tasksTime: { - data: tasksTimeChartData, + data: tasksTimeChartData || [], title: 'Tasks (time)', description: 'Time spent by individual tasks (hours)', }, assigneeBreakdown: { - data: choresAssigneeBreakdownChartData, + data: choresAssigneeBreakdownChartData || [], title: 'by Assignee', description: 'Tasks grouped by assignee', }, @@ -629,7 +787,7 @@ const UserActivites = () => { return ( { sx={{ width: { xs: '100%', lg: '350px' }, position: { xs: 'static', lg: 'sticky' }, - top: { lg: '20px' }, + top: { lg: '60px' }, alignSelf: { lg: 'flex-start' }, maxHeight: { lg: 'calc(100vh - 40px)' }, overflowY: { lg: 'auto' }, @@ -880,12 +1038,26 @@ const UserActivites = () => { > {/* Charts Container */} {/* Main Chart */} @@ -894,9 +1066,20 @@ const UserActivites = () => { display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'flex-start', textAlign: 'center', - minHeight: { lg: '400px' }, + minHeight: { + lg: + chartData[selectedChart].data.length <= 3 + ? '350px' + : '450px', + }, // Dynamic height based on legend needs + maxHeight: { + lg: + chartData[selectedChart].data.length <= 3 + ? '400px' + : '500px', + }, }} > @@ -907,14 +1090,16 @@ const UserActivites = () => { {renderPieChart( chartData[selectedChart].data, - 240, + 300, // Increased size for better chart container true, selectedChart, )} From 49a9055a7356ec4269856956dcc2331a9805e1e9 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 20 Jul 2025 10:41:28 -0400 Subject: [PATCH 2/2] chore: bump version to 0.1.106 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cec8cf..2a1518d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "0.1.105", + "version": "0.1.106", "type": "module", "engines": { "node": ">=20.0.0",