Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/components/BodyWeight/WeightChart/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
Expand Down
54 changes: 47 additions & 7 deletions src/components/BodyWeight/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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));
Expand All @@ -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(
<QueryClientProvider client={testQueryClient}>
<BodyWeight />
</QueryClientProvider>
);

// 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();
});


});
12 changes: 10 additions & 2 deletions src/components/BodyWeight/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<FilterType>('lastYear');
const weightyQuery = useBodyWeightQuery(filter);
const handleFilterChange = (newFilter: FilterType) => {
setFilter(newFilter);
};

return weightyQuery.isLoading
? <LoadingPlaceholder />
: <WgerContainerRightSidebar
title={t("weight")}
mainContent={<Stack spacing={2}>
<FilterButtons currentFilter={filter} onFilterChange={handleFilterChange} />
{weightyQuery.data!.length === 0 && <OverviewEmpty />}
<WeightChart weights={weightyQuery.data!} />
<Box sx={{ mt: 4 }} />
Expand Down
7 changes: 4 additions & 3 deletions src/components/BodyWeight/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}

Expand Down
57 changes: 57 additions & 0 deletions src/components/BodyWeight/widgets/FilterButtons.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<FilterButtons
currentFilter={currentFilter}
onFilterChange={onFilterChange}
/>
);
};

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');
});
});
69 changes: 69 additions & 0 deletions src/components/BodyWeight/widgets/FilterButtons.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ButtonGroup variant="outlined" sx={{ mb: 2 }}>
<Button
onClick={() => handleFilterChange('')}
color={currentFilter === '' ? 'primary' : 'inherit'}
variant={currentFilter === '' ? 'contained' : 'outlined'}
sx={{ fontFamily: theme.typography.fontFamily }}
>
{t('all')}
</Button>
<Button
onClick={() => handleFilterChange('lastYear')}
color={currentFilter === 'lastYear' ? 'primary' : 'inherit'}
variant={currentFilter === 'lastYear' ? 'contained' : 'outlined'}
sx={{ fontFamily: theme.typography.fontFamily }}
>
{t('lastYear')}
</Button>
<Button
onClick={() => handleFilterChange('lastHalfYear')}
color={currentFilter === 'lastHalfYear' ? 'primary' : 'inherit'}
variant={currentFilter === 'lastHalfYear' ? 'contained' : 'outlined'}
sx={{ fontFamily: theme.typography.fontFamily }}
>
{t('lastHalfYear')}
</Button>
<Button
onClick={() => handleFilterChange('lastMonth')}
color={currentFilter === 'lastMonth' ? 'primary' : 'inherit'}
variant={currentFilter === 'lastMonth' ? 'contained' : 'outlined'}
sx={{ fontFamily: theme.typography.fontFamily }}
>
{t('lastMonth')}
</Button>
<Button
onClick={() => handleFilterChange('lastWeek')}
color={currentFilter === 'lastWeek' ? 'primary' : 'inherit'}
variant={currentFilter === 'lastWeek' ? 'contained' : 'outlined'}
sx={{ fontFamily: theme.typography.fontFamily }}
>
{t('lastWeek')}
</Button>
</ButtonGroup>
);
};
2 changes: 1 addition & 1 deletion src/components/Dashboard/WeightCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
? <LoadingPlaceholder />
Expand Down
11 changes: 8 additions & 3 deletions src/services/weight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WeightEntry[]> => {
const url = makeUrl(WEIGHT_PATH, { query: { ordering: '-date', limit: 900 } });
export const getWeights = async (filter: FilterType = ''): Promise<WeightEntry[]> => {

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<ResponseType<ApiBodyWeightType>>(url, {
headers: makeHeader(),
});
Expand Down
31 changes: 31 additions & 0 deletions src/utils/date.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { calculatePastDate } from "utils/date";
import { dateToYYYYMMDD } from "utils/date";

describe("test date utility", () => {
Expand All @@ -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');
});
});
33 changes: 33 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FilterType } from "components/BodyWeight/widgets/FilterButtons";

/*
* Util function that converts a date to a YYYY-MM-DD string
*/
Expand Down Expand Up @@ -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<FilterType, (() => 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);
}
Loading