diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 60a47388..5adbf3cc 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -10,6 +10,11 @@ "close": "Close", "difference": "Difference", "days": "Days", + "all": "All", + "lastYear": "Last Year", + "lastHalfYear": "Last 6 Months", + "lastMonth": "Last Month", + "lastWeek": "Last Week", "licenses": { "authors": "Author(s)", "authorProfile": "Link to author website or profile, if available", diff --git a/src/components/BodyWeight/WeightChart/index.test.tsx b/src/components/BodyWeight/WeightChart/index.test.tsx index 34296b93..25f616da 100644 --- a/src/components/BodyWeight/WeightChart/index.test.tsx +++ b/src/components/BodyWeight/WeightChart/index.test.tsx @@ -28,7 +28,7 @@ describe("Test BodyWeight component", () => { test('renders without crashing', async () => { // Arrange - const weightData = [ + const weightData = [ new WeightEntry(new Date('2021-12-10'), 80, 1), new WeightEntry(new Date('2021-12-20'), 90, 2), ]; diff --git a/src/components/BodyWeight/index.test.tsx b/src/components/BodyWeight/index.test.tsx index 232e3631..0a655214 100644 --- a/src/components/BodyWeight/index.test.tsx +++ b/src/components/BodyWeight/index.test.tsx @@ -1,9 +1,11 @@ import { QueryClientProvider } from "@tanstack/react-query"; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { WeightEntry } from "components/BodyWeight/model"; import { getWeights } from "services"; import { testQueryClient } from "tests/queryClient"; import { BodyWeight } from "./index"; +import axios from "axios"; +import { FilterType } from "./widgets/FilterButtons"; const { ResizeObserver } = window; @@ -29,13 +31,13 @@ describe("Test BodyWeight component", () => { jest.restoreAllMocks(); }); - test('renders without crashing', async () => { + // Arrange + const weightData = [ + new WeightEntry(new Date('2021-12-10'), 80, 1), + new WeightEntry(new Date('2021-12-20'), 90, 2), + ]; - // Arrange - const weightData = [ - new WeightEntry(new Date('2021-12-10'), 80, 1), - new WeightEntry(new Date('2021-12-20'), 90, 2), - ]; + test('renders without crashing', async () => { // @ts-ignore getWeights.mockImplementation(() => Promise.resolve(weightData)); @@ -57,4 +59,42 @@ describe("Test BodyWeight component", () => { const textElement2 = await screen.findByText("90"); expect(textElement2).toBeInTheDocument(); }); + + + test('changes filter and updates displayed data', async () => { + + // Mock the getWeights response based on the filter + // @ts-ignore + getWeights.mockImplementation((filter: FilterType) => { + if (filter === 'lastYear') { + return Promise.resolve(weightData); + } else if (filter === 'lastMonth') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + render( + + + + ); + + // Initially should display data for last year + expect(await screen.findByText("80")).toBeInTheDocument(); + expect(await screen.findByText("90")).toBeInTheDocument(); + + // Change filter to 'lastMonth' + const filterButton = screen.getByRole('button', { name: /lastMonth/i }); + fireEvent.click(filterButton); + + // Expect getWeights to be called with 'lastMonth' + expect(getWeights).toHaveBeenCalledWith('lastMonth'); + + // Check that entries for last year are no longer in the document + expect(screen.queryByText("80")).not.toBeInTheDocument(); + expect(screen.queryByText("90")).not.toBeInTheDocument(); + }); + + }); diff --git a/src/components/BodyWeight/index.tsx b/src/components/BodyWeight/index.tsx index 782d0679..ffcee958 100644 --- a/src/components/BodyWeight/index.tsx +++ b/src/components/BodyWeight/index.tsx @@ -1,4 +1,4 @@ -import { Box, Stack } from "@mui/material"; +import { Box, Stack, Button, ButtonGroup } from "@mui/material"; import { useBodyWeightQuery } from "components/BodyWeight/queries"; import { WeightTable } from "components/BodyWeight/Table"; import { WeightChart } from "components/BodyWeight/WeightChart"; @@ -7,16 +7,24 @@ import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget" import { WgerContainerRightSidebar } from "components/Core/Widgets/Container"; import { OverviewEmpty } from "components/Core/Widgets/OverviewEmpty"; import { useTranslation } from "react-i18next"; +import { FilterButtons, FilterType } from "components/BodyWeight/widgets/FilterButtons"; +import { useState } from "react"; + export const BodyWeight = () => { const [t] = useTranslation(); - const weightyQuery = useBodyWeightQuery(); + const [filter, setFilter] = useState('lastYear'); + const weightyQuery = useBodyWeightQuery(filter); + const handleFilterChange = (newFilter: FilterType) => { + setFilter(newFilter); + }; return weightyQuery.isLoading ? : + {weightyQuery.data!.length === 0 && } diff --git a/src/components/BodyWeight/queries/index.ts b/src/components/BodyWeight/queries/index.ts index 22342970..5ee0d9f3 100644 --- a/src/components/BodyWeight/queries/index.ts +++ b/src/components/BodyWeight/queries/index.ts @@ -3,12 +3,13 @@ import { WeightEntry } from "components/BodyWeight/model"; import { createWeight, deleteWeight, getWeights, updateWeight, } from "services"; import { QueryKey, } from "utils/consts"; import { number } from "yup"; +import { FilterType } from "../widgets/FilterButtons"; -export function useBodyWeightQuery() { +export function useBodyWeightQuery(filter: FilterType = '') { return useQuery({ - queryKey: [QueryKey.BODY_WEIGHT], - queryFn: getWeights + queryKey: [QueryKey.BODY_WEIGHT, filter], + queryFn: () => getWeights(filter), }); } diff --git a/src/components/BodyWeight/widgets/FilterButtons.test.tsx b/src/components/BodyWeight/widgets/FilterButtons.test.tsx new file mode 100644 index 00000000..f17f140e --- /dev/null +++ b/src/components/BodyWeight/widgets/FilterButtons.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { FilterButtons, FilterButtonsProps, FilterType } from './FilterButtons'; + +describe('FilterButtons Component', () => { + const onFilterChange = jest.fn(); + + const renderComponent = (currentFilter: FilterType) => { + render( + + ); + }; + + afterEach(() => { + onFilterChange.mockClear(); + }); + + test('renders all filter buttons', () => { + renderComponent(''); + const buttonLabels = ['all', 'lastYear', 'lastHalfYear', 'lastMonth', 'lastWeek']; + buttonLabels.forEach(label => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + }); + + test('applies primary color and contained variant to the active filter button', () => { + renderComponent('lastMonth'); + const activeButton = screen.getByText('lastMonth'); + expect(activeButton).toHaveClass('MuiButton-containedPrimary'); + }); + + test('calls onFilterChange with correct value when a button is clicked', () => { + renderComponent(''); + const lastYearButton = screen.getByText('lastYear'); + + fireEvent.click(lastYearButton); + expect(onFilterChange).toHaveBeenCalledWith('lastYear'); + }); + + test('does not trigger onFilterChange when clicking the currently active filter button', () => { + renderComponent('lastYear'); + const lastYearButton = screen.getByText('lastYear'); + + fireEvent.click(lastYearButton); + expect(onFilterChange).not.toHaveBeenCalled(); + }); + + test('displays correct default style for inactive filter buttons', () => { + renderComponent(''); + const inactiveButton = screen.getByText('lastYear'); + expect(inactiveButton).toHaveClass('MuiButton-outlined'); + }); +}); diff --git a/src/components/BodyWeight/widgets/FilterButtons.tsx b/src/components/BodyWeight/widgets/FilterButtons.tsx new file mode 100644 index 00000000..1cdc62aa --- /dev/null +++ b/src/components/BodyWeight/widgets/FilterButtons.tsx @@ -0,0 +1,69 @@ +import { Button, ButtonGroup } from "@mui/material"; +import { useTheme } from '@mui/material/styles'; +import { useTranslation } from "react-i18next"; + +export type FilterType = 'lastYear' | 'lastHalfYear' | 'lastMonth' | 'lastWeek' | ''; + +export interface FilterButtonsProps { + currentFilter: FilterType; + onFilterChange: (newFilter: FilterType) => void; +} + +export const FilterButtons = ({ currentFilter, onFilterChange }: FilterButtonsProps) => { + + const [t] = useTranslation(); + + const theme = useTheme(); + + // Won't call onFilterChange if the filter stays the same + const handleFilterChange = (newFilter: FilterType) => { + if (currentFilter !== newFilter) { + onFilterChange(newFilter); + } + }; + + return ( + + + + + + + + ); +}; diff --git a/src/components/Dashboard/WeightCard.tsx b/src/components/Dashboard/WeightCard.tsx index 5a9d0ff4..00d53ac2 100644 --- a/src/components/Dashboard/WeightCard.tsx +++ b/src/components/Dashboard/WeightCard.tsx @@ -16,7 +16,7 @@ import { makeLink, WgerLink } from "utils/url"; export const WeightCard = () => { const [t] = useTranslation(); - const weightyQuery = useBodyWeightQuery(); + const weightyQuery = useBodyWeightQuery('lastYear'); return (<>{weightyQuery.isLoading ? diff --git a/src/services/weight.ts b/src/services/weight.ts index 87e2033a..f0fe6e32 100644 --- a/src/services/weight.ts +++ b/src/services/weight.ts @@ -3,14 +3,19 @@ import { WeightAdapter, WeightEntry } from "components/BodyWeight/model"; import { ApiBodyWeightType } from 'types'; import { makeHeader, makeUrl } from "utils/url"; import { ResponseType } from "./responseType"; +import { FilterType } from '../components/BodyWeight/widgets/FilterButtons'; +import { calculatePastDate } from '../utils/date'; export const WEIGHT_PATH = 'weightentry'; /* - * Fetch all weight entries + * Fetch weight entries based on filter value */ -export const getWeights = async (): Promise => { - const url = makeUrl(WEIGHT_PATH, { query: { ordering: '-date', limit: 900 } }); +export const getWeights = async (filter: FilterType = ''): Promise => { + + const date__gte = calculatePastDate(filter); + + const url = makeUrl(WEIGHT_PATH, { query: { ordering: '-date', limit: 900, ...(date__gte && { date__gte }) } }); const { data: receivedWeights } = await axios.get>(url, { headers: makeHeader(), }); diff --git a/src/utils/date.test.ts b/src/utils/date.test.ts index 0e3bb82c..f91800ff 100644 --- a/src/utils/date.test.ts +++ b/src/utils/date.test.ts @@ -1,3 +1,4 @@ +import { calculatePastDate } from "utils/date"; import { dateToYYYYMMDD } from "utils/date"; describe("test date utility", () => { @@ -19,3 +20,33 @@ describe("test date utility", () => { }); + + + + +describe('calculatePastDate', () => { + + it('should return undefined for empty string filter', () => { + expect(calculatePastDate('', new Date('2023-08-14'))).toBeUndefined(); + }); + + it('should return the correct date for lastWeek filter', () => { + const result = calculatePastDate('lastWeek', new Date('2023-02-14')); + expect(result).toStrictEqual('2023-02-07'); + }); + + it('should return the correct date for lastMonth filter', () => { + const result = calculatePastDate('lastMonth', new Date('2023-02-14')); + expect(result).toStrictEqual('2023-01-14'); + }); + + it('should return the correct date for lastHalfYear filter', () => { + const result = calculatePastDate('lastHalfYear', new Date('2023-08-14')); + expect(result).toStrictEqual('2023-02-14'); + }); + + it('should return the correct date for lastYear filter', () => { + const result = calculatePastDate('lastYear', new Date('2023-02-14')); + expect(result).toStrictEqual('2022-02-14'); + }); +}); \ No newline at end of file diff --git a/src/utils/date.ts b/src/utils/date.ts index 37c8ee1b..ed34c431 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,3 +1,5 @@ +import { FilterType } from "components/BodyWeight/widgets/FilterButtons"; + /* * Util function that converts a date to a YYYY-MM-DD string */ @@ -74,4 +76,35 @@ export function HHMMToDateTime(time: string | null) { dateTime.setHours(parseInt(hour)); dateTime.setMinutes(parseInt(minute)); return dateTime; +} + +/* + * Util function that calculates a date in the past based on a string filter + * and returns it as a YYYY-MM-DD string for API queries. + * + * @param filter - A string representing the desired time period (e.g., 'lastWeek', 'lastMonth') + * @param currentDate - (Optional) The current date to base calculations on. Defaults to `new Date()`. + * This parameter allows for testing or custom date bases. + * @returns - Date string in the format YYYY-MM-DD or undefined for no filtering + */ +export function calculatePastDate(filter: FilterType, currentDate: Date = new Date()): string | undefined { + + // Dictionary for filters + const filterMap: Record void) | undefined> = { + lastWeek: () => currentDate.setDate(currentDate.getDate() - 7), + lastMonth: () => currentDate.setMonth(currentDate.getMonth() - 1), + lastHalfYear: () => currentDate.setMonth(currentDate.getMonth() - 6), + lastYear: () => currentDate.setFullYear(currentDate.getFullYear() - 1), + '': undefined + }; + + // Execute the corresponding function for the filter + const applyFilter = filterMap[filter]; + if (applyFilter) { + applyFilter(); + } else { + return undefined; + } + + return dateToYYYYMMDD(currentDate); } \ No newline at end of file