Skip to content

Commit

Permalink
Merge pull request #828 from CruGlobal/7532-coaching-weekly-report
Browse files Browse the repository at this point in the history
[MPDX-7532] Add coaching weekly report
  • Loading branch information
canac authored Jan 9, 2024
2 parents a97f7c4 + 602164c commit 21f91e3
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/components/Coaching/CoachingDetail/CoachingDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from './LoadCoachingDetail.generated';
import { MonthlyCommitment } from './MonthlyCommitment/MonthlyCommitment';
import { SideContainerText } from './StyledComponents';
import { WeeklyReport } from './WeeklyReport/WeeklyReport';
import { getLastNewsletter } from './helpers';

export enum CoachingPeriodEnum {
Expand Down Expand Up @@ -346,6 +347,7 @@ export const CoachingDetail: React.FC<CoachingDetailProps> = ({
currency={accountListData?.currency}
primaryAppeal={accountListData?.primaryAppeal ?? undefined}
/>
<WeeklyReport accountListId={accountListId} />
</CoachingItemContainer>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query WeeklyReports($accountListId: ID!) {
coachingAnswerSets(accountListId: $accountListId, completed: true) {
id
answers {
id
response
question {
id
position
prompt
}
}
completedAt
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DateTime, Settings } from 'luxon';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { WeeklyReport } from './WeeklyReport';
import { WeeklyReportsQuery } from './WeeklyReport.generated';

const generateAnswer = (position: number, suffix: string) => ({
response: `Answer ${position}${suffix}`,
question: {
position,
prompt: `Question ${position}${suffix}`,
},
});

const mocks = {
WeeklyReports: {
coachingAnswerSets: [
{
completedAt: DateTime.local(2023, 11, 1).toISO(),
answers: [
generateAnswer(3, 'a'),
generateAnswer(1, 'a'),
generateAnswer(2, 'a'),
],
},
{
completedAt: DateTime.local(2022, 10, 1).toISO(),
answers: [
generateAnswer(3, 'b'),
generateAnswer(1, 'b'),
generateAnswer(2, 'b'),
],
},
],
},
};

const accountListId = 'account-list-1';

const TestComponent: React.FC = () => (
<GqlMockedProvider<{ WeeklyReports: WeeklyReportsQuery }> mocks={mocks}>
<WeeklyReport accountListId={accountListId} />
</GqlMockedProvider>
);

describe('WeeklyReport', () => {
beforeEach(() => {
Settings.now = () => new Date(2023, 12, 1).valueOf();
});

describe('previous/next buttons', () => {
it('both buttons are disabled until the reports load', () => {
const { getAllByTestId, getByRole } = render(<TestComponent />);

expect(getAllByTestId('MultilineSkeletonLine')).not.toHaveLength(0);
expect(getByRole('button', { name: 'Next' })).toBeDisabled();
expect(getByRole('button', { name: 'Previous' })).toBeDisabled();
});

it('next button is disabled when on the latest report', async () => {
const { getByRole, getByTestId, queryByTestId } = render(
<TestComponent />,
);

await waitFor(() =>
expect(queryByTestId('MultilineSkeletonLine')).not.toBeInTheDocument(),
);

const next = getByRole('button', { name: 'Next' });
const previous = getByRole('button', { name: 'Previous' });
const completed = getByTestId('CompletedText');

expect(next).toBeDisabled();
expect(previous).not.toBeDisabled();
expect(completed).toHaveTextContent('Nov 1');

userEvent.click(previous);
expect(next).not.toBeDisabled();
expect(previous).toBeDisabled();
expect(completed).toHaveTextContent('Oct 1, 2022');

userEvent.click(next);
expect(next).toBeDisabled();
expect(previous).not.toBeDisabled();
expect(completed).toHaveTextContent('Nov 1');
});
});

it('renders the questions and answers sorted by position', async () => {
const { findAllByTestId } = render(<TestComponent />);

const answers = await findAllByTestId('Answer');
expect(answers).toHaveLength(3);
expect(answers[0]).toHaveTextContent('Question 1aAnswer 1a');
expect(answers[1]).toHaveTextContent('Question 2aAnswer 2a');
expect(answers[2]).toHaveTextContent('Question 3aAnswer 3a');
});

it('renders placeholder when there are no reports', async () => {
const { findByText } = render(
<GqlMockedProvider<{ WeeklyReports: WeeklyReportsQuery }>
mocks={{ WeeklyReports: { coachingAnswerSets: [] } }}
>
<WeeklyReport accountListId={accountListId} />
</GqlMockedProvider>,
);

expect(await findByText('No completed reports found')).toBeInTheDocument();
});
});
156 changes: 156 additions & 0 deletions src/components/Coaching/CoachingDetail/WeeklyReport/WeeklyReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useMemo, useState } from 'react';
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import {
Button,
ButtonGroup,
CardContent,
CardHeader,
Typography,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import AnimatedCard from 'src/components/AnimatedCard';
import { MultilineSkeleton } from 'src/components/Shared/MultilineSkeleton';
import { useLocale } from 'src/hooks/useLocale';
import { dateFormat, dateFormatWithoutYear } from 'src/lib/intlFormat';
import { useWeeklyReportsQuery } from './WeeklyReport.generated';

const Header = styled(Typography)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(2),
'@media (max-width: 900px)': {
flexDirection: 'column',
alignItems: 'center',
},
}));

const CompletedText = styled('span')({
textAlign: 'center',
flex: 1,
});

const StyledButton = styled(Button)({
width: '6rem',
});

const ContentContainer = styled(CardContent)(({ theme }) => ({
padding: theme.spacing(2),
overflowX: 'scroll',
}));

const AnswersContainer = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: theme.spacing(4),
'@media (max-width: 1150px)': {
gridTemplateColumns: 'repeat(2, 1fr)',
},
'@media (max-width: 900px)': {
gridTemplateColumns: '1fr',
},
}));

const AnswerContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));

const QuestionText = styled('p')({
fontWeight: 'bold',
});

interface WeeklyReportProps {
accountListId: string;
}

export const WeeklyReport: React.FC<WeeklyReportProps> = ({
accountListId,
}) => {
const { t } = useTranslation();
const locale = useLocale();

const [reportIndex, setReportIndex] = useState(0);

const { data, loading } = useWeeklyReportsQuery({
variables: { accountListId },
});
const reports = data?.coachingAnswerSets ?? [];
const report = reports[reportIndex];

// Sort the answers by their position
const answers = useMemo(() => {
if (!report) {
return [];
}

const answers = report.answers.slice(0);
answers.sort(
(answer1, answer2) =>
answer1.question.position - answer2.question.position,
);
return answers;
}, [report]);

const completedDate = useMemo(() => {
if (!report?.completedAt) {
return null;
}

const date = DateTime.fromISO(report.completedAt);
const format =
date.year === DateTime.now().year ? dateFormatWithoutYear : dateFormat;
return format(date, locale);
}, [report, locale]);

return (
<AnimatedCard>
<CardHeader
title={
<Header variant="h6">
<span>{t('Weekly Report')}</span>
<CompletedText data-testid="CompletedText">
{completedDate}
</CompletedText>
<ButtonGroup>
<StyledButton
onClick={() => setReportIndex(reportIndex + 1)}
disabled={reportIndex >= reports.length - 1}
>
<ChevronLeft />
{t('Previous')}
</StyledButton>
<StyledButton
onClick={() => setReportIndex(reportIndex - 1)}
disabled={reportIndex === 0}
>
{t('Next')}
<ChevronRight />
</StyledButton>
</ButtonGroup>
</Header>
}
/>
<ContentContainer>
{loading ? (
<MultilineSkeleton lines={4} height={80} />
) : reports.length === 0 ? (
t('No completed reports found')
) : (
<AnswersContainer>
{answers.map((answer) => (
<AnswerContainer key={answer.id} data-testid="Answer">
<QuestionText role="heading">
{answer.question.prompt}
</QuestionText>
<p>{answer.response}</p>
</AnswerContainer>
))}
</AnswersContainer>
)}
</ContentContainer>
</AnimatedCard>
);
};
2 changes: 1 addition & 1 deletion src/components/Shared/MultilineSkeleton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ describe('MultilineSkeleton', () => {
it('renders the specific number of lines', () => {
const { queryAllByTestId } = render(<MultilineSkeleton lines={3} />);

expect(queryAllByTestId('Line')).toHaveLength(3);
expect(queryAllByTestId('MultilineSkeletonLine')).toHaveLength(3);
});
});
6 changes: 5 additions & 1 deletion src/components/Shared/MultilineSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export const MultilineSkeleton: React.FC<MultilineSkeletonProps> = ({
}) => (
<>
{new Array(lines).fill(undefined).map((_, index) => (
<StyledSkeleton key={index} data-testid="Line" {...props} />
<StyledSkeleton
key={index}
data-testid="MultilineSkeletonLine"
{...props}
/>
))}
</>
);

0 comments on commit 21f91e3

Please sign in to comment.