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