richie-education 2.25.0-b2.dev69 → 2.25.0-b2.dev77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +95 -129
  2. package/js/components/ContractFrame/OrganizationContractFrame.tsx +10 -2
  3. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +37 -6
  4. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +11 -2
  5. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +2 -0
  6. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +50 -9
  7. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +8 -2
  8. package/js/types/Joanie.ts +1 -0
  9. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
  10. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.spec.tsx → CourseEnrolling/index.spec.tsx} +44 -13
  11. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +2 -2
  12. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +38 -57
  13. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +7 -19
  14. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  15. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +76 -24
  16. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +1 -1
  17. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +3 -1
  18. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +136 -30
  19. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +60 -12
  20. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -2
  21. package/package.json +1 -1
@@ -6,9 +6,14 @@ import { IntlProvider } from 'react-intl';
6
6
  import fetchMock from 'fetch-mock';
7
7
  import { QueryStateFactory } from 'utils/test/factories/reactQuery';
8
8
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
- import { ContractFactory, OrganizationFactory } from 'utils/test/factories/joanie';
9
+ import {
10
+ ContractFactory,
11
+ CourseProductRelationFactory,
12
+ OrganizationFactory,
13
+ } from 'utils/test/factories/joanie';
10
14
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
11
15
  import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
16
+ import { isCourseProductRelation } from 'types/Joanie';
12
17
  import { Props } from './AbstractContractFrame';
13
18
  import { OrganizationContractFrame } from '.';
14
19
 
@@ -70,132 +75,93 @@ describe('OrganizationContractFrame', () => {
70
75
  fetchMock.restore();
71
76
  });
72
77
 
73
- it('should implement AbstractContractFrame for organization', async () => {
74
- const organization = OrganizationFactory().one();
75
- const contract = ContractFactory().one();
76
- const isOpen = faker.datatype.boolean();
77
-
78
- const expectedUrls = {
79
- getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/`,
80
- checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contract.id}`,
81
- };
82
-
83
- fetchMock
84
- .get(expectedUrls.getInvitationLink, {
85
- invitation_link: faker.internet.url(),
86
- contract_ids: [contract.id],
87
- })
88
- .get(expectedUrls.checkSignature, { results: [contract] });
89
-
90
- const handleDone = jest.fn();
91
- const handleClose = jest.fn();
92
-
93
- const client = createTestQueryClient({
94
- user: true,
95
- queriesCallback: (queries) => {
96
- // Push contract and orders queries
97
- queries.push(QueryStateFactory(['user', 'organization_contracts'], { data: [] }));
98
- },
99
- });
100
-
101
- let contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
102
- expect(contractsQueryState?.isInvalidated).toBe(false);
103
-
104
- await act(async () => {
105
- render(
106
- <Wrapper client={client}>
107
- <OrganizationContractFrame
108
- organizationId={organization.id}
109
- isOpen={isOpen}
110
- onDone={handleDone}
111
- onClose={handleClose}
112
- />
113
- </Wrapper>,
114
- );
115
- });
116
-
117
- // isOpen should be passed down to AbstractContractFrame
118
- const contractFrame = await screen.getByTestId('AbstractContractFrame');
119
- expect(contractFrame).toHaveAttribute('data-is-open', String(isOpen));
120
-
121
- // getInvitationLink should post on order/submit-for-signature endpoint
122
- expect(fetchMock.called(expectedUrls.getInvitationLink)).toBe(true);
123
-
124
- // checkSignature should get on order endpoint
125
- expect(fetchMock.called(expectedUrls.checkSignature)).toBe(true);
126
-
127
- // onDone should be tweaked to invalidate the user orders and contracts queries
128
- // and passed down to AbstractContractFrame
129
- contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
130
- expect(contractsQueryState?.isInvalidated).toBe(true);
131
- expect(handleDone).toHaveBeenCalledTimes(1);
132
-
133
- // onClose should be passed down to AbstractContractFrame
134
- expect(handleClose).toHaveBeenCalledTimes(1);
135
- });
136
-
137
- it('should implement AbstractContractFrame for organization with a list of contract ids', async () => {
138
- const organization = OrganizationFactory().one();
139
- const contracts = ContractFactory().many(2);
140
- const isOpen = faker.datatype.boolean();
141
-
142
- const expectedUrls = {
143
- getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/?contracts_ids=${contracts[0].id}&contracts_ids=${contracts[1].id}`,
144
- checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
145
- };
146
-
147
- fetchMock
148
- .get(expectedUrls.getInvitationLink, {
149
- invitation_link: faker.internet.url(),
150
- contract_ids: contracts.map((contract) => contract.id),
151
- })
152
- .get(expectedUrls.checkSignature, { results: [contracts] });
153
-
154
- const handleDone = jest.fn();
155
- const handleClose = jest.fn();
156
-
157
- const client = createTestQueryClient({
158
- user: true,
159
- queriesCallback: (queries) => {
160
- // Push contract and orders queries
161
- queries.push(QueryStateFactory(['user', 'organization_contracts'], { data: [] }));
162
- },
163
- });
164
-
165
- let contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
166
- expect(contractsQueryState?.isInvalidated).toBe(false);
167
-
168
- await act(async () => {
169
- render(
170
- <Wrapper client={client}>
171
- <OrganizationContractFrame
172
- organizationId={organization.id}
173
- contractIds={contracts.map((contract) => contract.id)}
174
- isOpen={isOpen}
175
- onDone={handleDone}
176
- onClose={handleClose}
177
- />
178
- </Wrapper>,
179
- );
180
- });
181
-
182
- // isOpen should be passed down to AbstractContractFrame
183
- const contractFrame = await screen.getByTestId('AbstractContractFrame');
184
- expect(contractFrame).toHaveAttribute('data-is-open', String(isOpen));
185
-
186
- // getInvitationLink should post on order/submit-for-signature endpoint
187
- expect(fetchMock.called(expectedUrls.getInvitationLink)).toBe(true);
188
-
189
- // checkSignature should get on order endpoint
190
- expect(fetchMock.called(expectedUrls.checkSignature)).toBe(true);
191
-
192
- // onDone should be tweaked to invalidate the user orders and contracts queries
193
- // and passed down to AbstractContractFrame
194
- contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
195
- expect(contractsQueryState?.isInvalidated).toBe(true);
196
- expect(handleDone).toHaveBeenCalledTimes(1);
197
-
198
- // onClose should be passed down to AbstractContractFrame
199
- expect(handleClose).toHaveBeenCalledTimes(1);
200
- });
78
+ it.each([
79
+ {
80
+ label: 'contractList: undefined, courseProductRelation: undefined',
81
+ contractList: undefined,
82
+ courseProductRelation: undefined,
83
+ },
84
+ {
85
+ label: 'contractList: 2 Contract, courseProductRelation: undefined',
86
+ contractList: ContractFactory().many(2),
87
+ courseProductRelation: undefined,
88
+ },
89
+ {
90
+ label: 'contractList: undefined, courseProductRelation: one CourseProductRelation',
91
+ contractList: undefined,
92
+ courseProductRelation: CourseProductRelationFactory().one(),
93
+ },
94
+ ])(
95
+ 'should implement AbstractContractFrame for organization and $label',
96
+ async ({ contractList, courseProductRelation }) => {
97
+ const organization = OrganizationFactory().one();
98
+ const contracts = contractList || ContractFactory().many(2);
99
+ const isOpen = faker.datatype.boolean();
100
+
101
+ const invitationLinkQueryString =
102
+ courseProductRelation && isCourseProductRelation(courseProductRelation)
103
+ ? `?course_product_relation_ids=${courseProductRelation.id}`
104
+ : '';
105
+ const expectedUrls = {
106
+ getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
107
+ checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
108
+ };
109
+
110
+ fetchMock
111
+ .get(expectedUrls.getInvitationLink, {
112
+ invitation_link: faker.internet.url(),
113
+ contract_ids: contracts.map((contract) => contract.id),
114
+ })
115
+ .get(expectedUrls.checkSignature, { results: [contracts] });
116
+
117
+ const handleDone = jest.fn();
118
+ const handleClose = jest.fn();
119
+
120
+ const client = createTestQueryClient({
121
+ user: true,
122
+ queriesCallback: (queries) => {
123
+ // Push contract and orders queries
124
+ queries.push(QueryStateFactory(['user', 'organization_contracts'], { data: [] }));
125
+ },
126
+ });
127
+
128
+ let contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
129
+ expect(contractsQueryState?.isInvalidated).toBe(false);
130
+
131
+ await act(async () => {
132
+ render(
133
+ <Wrapper client={client}>
134
+ <OrganizationContractFrame
135
+ organizationId={organization.id}
136
+ courseProductRelationIds={
137
+ courseProductRelation ? [courseProductRelation.id] : undefined
138
+ }
139
+ isOpen={isOpen}
140
+ onDone={handleDone}
141
+ onClose={handleClose}
142
+ />
143
+ </Wrapper>,
144
+ );
145
+ });
146
+
147
+ // isOpen should be passed down to AbstractContractFrame
148
+ const contractFrame = screen.getByTestId('AbstractContractFrame');
149
+ expect(contractFrame).toHaveAttribute('data-is-open', String(isOpen));
150
+
151
+ // getInvitationLink should post on order/submit-for-signature endpoint
152
+ expect(fetchMock.called(expectedUrls.getInvitationLink)).toBe(true);
153
+
154
+ // checkSignature should get on order endpoint
155
+ expect(fetchMock.called(expectedUrls.checkSignature)).toBe(true);
156
+
157
+ // onDone should be tweaked to invalidate the user orders and contracts queries
158
+ // and passed down to AbstractContractFrame
159
+ contractsQueryState = client.getQueryState(['user', 'organization_contracts']);
160
+ expect(contractsQueryState?.isInvalidated).toBe(true);
161
+ expect(handleDone).toHaveBeenCalledTimes(1);
162
+
163
+ // onClose should be passed down to AbstractContractFrame
164
+ expect(handleClose).toHaveBeenCalledTimes(1);
165
+ },
166
+ );
201
167
  });
@@ -4,14 +4,21 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
4
4
  import AbstractContractFrame, {
5
5
  AbstractProps,
6
6
  } from 'components/ContractFrame/AbstractContractFrame';
7
- import { Contract } from 'types/Joanie';
7
+ import { Contract, CourseProductRelation } from 'types/Joanie';
8
8
 
9
9
  interface Props extends AbstractProps {
10
10
  contractIds?: Contract['id'][];
11
11
  organizationId: string;
12
+ courseProductRelationIds?: CourseProductRelation['id'][];
12
13
  }
13
14
 
14
- const OrganizationContractFrame = ({ organizationId, contractIds, onDone, ...props }: Props) => {
15
+ const OrganizationContractFrame = ({
16
+ organizationId,
17
+ courseProductRelationIds = [],
18
+ contractIds,
19
+ onDone,
20
+ ...props
21
+ }: Props) => {
15
22
  const api = useJoanieApi();
16
23
  const queryClient = useQueryClient();
17
24
  const [contractIdsToCheck, setContractIdsToCheck] = useState(contractIds);
@@ -22,6 +29,7 @@ const OrganizationContractFrame = ({ organizationId, contractIds, onDone, ...pro
22
29
  be signed. We need to keep track of these ids to check if all contracts have been signed.
23
30
  */
24
31
  const response = await api.organizations.contracts.getSignatureLinks({
32
+ course_product_relation_ids: courseProductRelationIds,
25
33
  organization_id: organizationId,
26
34
  contracts_ids: contractIds,
27
35
  });
@@ -76,18 +76,19 @@ describe('pages/TeacherDashboardContracts', () => {
76
76
  student_signed_on: Date.toString(),
77
77
  organization_signed_on: Date.toString(),
78
78
  }).many(3);
79
- const organization = OrganizationFactory().one();
79
+ const organizations = OrganizationFactory().many(2);
80
+ const defaultOrganization = organizations[0];
80
81
 
81
82
  // OrganizationContractFilter request all organizations forwho the user have access
82
- fetchMock.get(`https://joanie.test/api/v1.0/organizations/`, [organization]);
83
+ fetchMock.get(`https://joanie.test/api/v1.0/organizations/`, organizations);
83
84
  // TeacherDashboardContracts request a paginated list of contracts to display
84
85
  fetchMock.get(
85
- `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=signed&page=1&page_size=25`,
86
+ `https://joanie.test/api/v1.0/organizations/${defaultOrganization.id}/contracts/?course_product_relation_id=2&signature_state=signed&page=1&page_size=25`,
86
87
  { results: contracts, count: 0, previous: null, next: null },
87
88
  );
88
89
  // useTeacherContractsToSign request all contract to sign, without pagination
89
90
  fetchMock.get(
90
- `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=half_signed`,
91
+ `https://joanie.test/api/v1.0/organizations/${defaultOrganization.id}/contracts/?signature_state=half_signed&course_product_relation_id=2`,
91
92
  { results: [], count: 0, previous: null, next: null },
92
93
  );
93
94
 
@@ -101,10 +102,10 @@ describe('pages/TeacherDashboardContracts', () => {
101
102
  await expectNoSpinner();
102
103
 
103
104
  // Organization filter should have been rendered
104
- const organizationFilter: HTMLInputElement = screen.getByRole('combobox', {
105
+ const organizationFilter: HTMLInputElement = await screen.findByRole('combobox', {
105
106
  name: 'Organization',
106
107
  });
107
- expect(organizationFilter).toHaveAttribute('value', organization.title);
108
+ expect(organizationFilter).toHaveAttribute('value', defaultOrganization.title);
108
109
 
109
110
  // Signature state filter should have been rendered
110
111
  const signatureStateFilter: HTMLInputElement = screen.getByRole('combobox', {
@@ -330,4 +331,34 @@ describe('pages/TeacherDashboardContracts', () => {
330
331
  await expectNoSpinner();
331
332
  await expectBannerError('An error occurred while fetching contracts. Please retry later.');
332
333
  });
334
+
335
+ it('should hide organization filter when user only have one organization', async () => {
336
+ const defaultOrganization = OrganizationFactory().one();
337
+ fetchMock.get('https://joanie.test/api/v1.0/organizations/', [defaultOrganization]);
338
+
339
+ const contracts = ContractFactory({
340
+ student_signed_on: Date.toString(),
341
+ abilities: { sign: true },
342
+ }).many(3);
343
+ fetchMock.get(
344
+ `https://joanie.test/api/v1.0/organizations/${defaultOrganization.id}/contracts/?signature_state=signed&page=1&page_size=25`,
345
+ { results: [], count: 0, previous: null, next: null },
346
+ );
347
+ fetchMock.get(
348
+ `https://joanie.test/api/v1.0/organizations/${defaultOrganization.id}/contracts/?signature_state=half_signed`,
349
+ { results: contracts, count: 3, previous: null, next: null },
350
+ );
351
+ render(<Wrapper path="/" initialEntry="" />);
352
+ await expectNoSpinner();
353
+
354
+ // Signature state filter should have been rendered
355
+ screen.getByRole('combobox', {
356
+ name: 'Signature state',
357
+ hidden: true,
358
+ });
359
+
360
+ // Organization filter should not have been rendered
361
+ const organizationFilter = screen.queryByRole('combobox', { name: 'Organization' });
362
+ expect(organizationFilter).not.toBeInTheDocument();
363
+ });
333
364
  });
@@ -9,6 +9,7 @@ import Banner, { BannerType } from 'components/Banner';
9
9
  import { PER_PAGE } from 'settings';
10
10
  import { ContractResourceQuery } from 'types/Joanie';
11
11
 
12
+ import { useOrganizations } from 'hooks/useOrganizations';
12
13
  import ContractFiltersBar from '../components/ContractFiltersBar';
13
14
  import useTeacherContractFilters, {
14
15
  TeacherDashboardContractsParams,
@@ -41,7 +42,15 @@ const TeacherDashboardContracts = () => {
41
42
  defaultPage: page ? parseInt(page, 10) : 1,
42
43
  pageSize: PER_PAGE.teacherContractList,
43
44
  });
44
- const { organizationId } = useParams<TeacherDashboardContractsParams>();
45
+ const { organizationId: routeOrganizationId } = useParams<TeacherDashboardContractsParams>();
46
+ // organization list is used to show/hide organization filter.
47
+ // when organizationId is in route's params this filter is always hidden.
48
+ // therefore we don't need to enable this query.
49
+ const {
50
+ items: organizationList,
51
+ states: { isFetched: isOrganizationListFetched },
52
+ } = useOrganizations(undefined, { enabled: !!routeOrganizationId });
53
+ const hasMultipleOrganizations = isOrganizationListFetched && organizationList.length > 1;
45
54
  const { initialFilters, filters, setFilters } = useTeacherContractFilters();
46
55
  const {
47
56
  items: contracts,
@@ -88,7 +97,7 @@ const TeacherDashboardContracts = () => {
88
97
  <ContractFiltersBar
89
98
  defaultValues={initialFilters}
90
99
  onFiltersChange={handleFiltersChange}
91
- hideFilterOrganization={!!organizationId}
100
+ hideFilterOrganization={!!(routeOrganizationId || !hasMultipleOrganizations)}
92
101
  />
93
102
  </div>
94
103
  <DataGrid
@@ -19,6 +19,7 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
19
19
 
20
20
  const canDownloadContracts = hasContractToDownload && !courseProductRelationId;
21
21
  const nbAvailableActions = [canSignContracts, canDownloadContracts].filter((val) => val).length;
22
+ const courseProductRelationIds = courseProductRelationId ? [courseProductRelationId] : undefined;
22
23
  return (
23
24
  nbAvailableActions > 0 && (
24
25
  <div
@@ -31,6 +32,7 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
31
32
  {canSignContracts && (
32
33
  <div>
33
34
  <SignOrganizationContractButton
35
+ courseProductRelationIds={courseProductRelationIds}
34
36
  organizationId={organizationId}
35
37
  contractToSignCount={contractsToSignCount}
36
38
  />
@@ -1,12 +1,13 @@
1
1
  import { faker } from '@faker-js/faker';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { IntlProvider } from 'react-intl';
4
3
  import { PropsWithChildren } from 'react';
4
+ import { IntlProvider } from 'react-intl';
5
5
  import { QueryClientProvider } from '@tanstack/react-query';
6
+ import fetchMock from 'fetch-mock';
7
+ import userEvent from '@testing-library/user-event';
6
8
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
9
  import JoanieApiProvider from 'contexts/JoanieApiContext';
8
10
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
9
-
10
11
  import SignOrganizationContractButton from '.';
11
12
 
12
13
  jest.mock('utils/context', () => ({
@@ -35,26 +36,27 @@ describe('TeacherDashboardContractsLayout/SignOrganizationContractButton', () =>
35
36
  });
36
37
 
37
38
  afterEach(() => {
38
- jest.resetAllMocks();
39
+ fetchMock.restore();
39
40
  });
40
41
 
41
- it("shouldn't render sign button and <OrganizationContractFrame/> when contractToSignCount > 0", () => {
42
+ it('should display sign button user have some contract to sign', () => {
42
43
  render(
43
44
  <Wrapper>
44
45
  <SignOrganizationContractButton
45
46
  organizationId={faker.string.uuid()}
46
- contractToSignCount={1}
47
+ contractToSignCount={12}
47
48
  />
48
49
  </Wrapper>,
49
50
  );
50
51
 
51
- expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
52
-
52
+ expect(
53
+ screen.getByRole('button', { name: 'Sign all pending contracts (12)' }),
54
+ ).toBeInTheDocument();
53
55
  const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
54
56
  expect(DashboardContractFramePortal).toHaveLength(1);
55
57
  });
56
58
 
57
- it("shouldn't only render <OrganizationContractFrame/> when contractToSignCount is 0", () => {
59
+ it("shouldn't display sign button user don't have some contract to sign", () => {
58
60
  render(
59
61
  <Wrapper>
60
62
  <SignOrganizationContractButton
@@ -67,8 +69,47 @@ describe('TeacherDashboardContractsLayout/SignOrganizationContractButton', () =>
67
69
  expect(
68
70
  screen.queryByRole('button', { name: /Sign all pending contracts/ }),
69
71
  ).not.toBeInTheDocument();
70
-
71
72
  const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
72
73
  expect(DashboardContractFramePortal).toHaveLength(1);
73
74
  });
75
+
76
+ it.each([
77
+ {
78
+ label: "organization's contracts",
79
+ organizationId: faker.string.uuid(),
80
+ courseProductRelationIds: undefined,
81
+ },
82
+ {
83
+ label: "organization's training contracts",
84
+ organizationId: faker.string.uuid(),
85
+ courseProductRelationIds: [faker.string.uuid()],
86
+ },
87
+ ])('should open $label frame on click', async ({ organizationId, courseProductRelationIds }) => {
88
+ render(
89
+ <Wrapper>
90
+ <SignOrganizationContractButton
91
+ organizationId={organizationId}
92
+ courseProductRelationIds={courseProductRelationIds}
93
+ contractToSignCount={12}
94
+ />
95
+ </Wrapper>,
96
+ );
97
+
98
+ const $button = screen.getByRole('button', { name: /Sign all pending contracts/ });
99
+ const user = userEvent.setup();
100
+
101
+ let getInvitationLinkUrl = `https://joanie.test/api/v1.0/organizations/${organizationId}/contracts-signature-link/`;
102
+ if (courseProductRelationIds) {
103
+ getInvitationLinkUrl += `?course_product_relation_ids=${courseProductRelationIds[0]}`;
104
+ }
105
+
106
+ fetchMock.get(getInvitationLinkUrl, {
107
+ invitation_link: 'https://dummysignaturebackend.fr',
108
+ contract_ids: [],
109
+ });
110
+ await user.click($button);
111
+
112
+ expect(screen.getByTestId('dashboard-contract-frame')).toBeInTheDocument();
113
+ expect(fetchMock.called(getInvitationLinkUrl)).toBe(true);
114
+ });
74
115
  });
@@ -3,7 +3,7 @@ import { defineMessages, FormattedMessage } from 'react-intl';
3
3
  import { Button } from '@openfun/cunningham-react';
4
4
  import { OrganizationContractFrame } from 'components/ContractFrame';
5
5
 
6
- import { Organization } from 'types/Joanie';
6
+ import { CourseProductRelation, Organization } from 'types/Joanie';
7
7
 
8
8
  const messages = defineMessages({
9
9
  signAllPendingContracts: {
@@ -14,11 +14,16 @@ const messages = defineMessages({
14
14
  });
15
15
 
16
16
  interface Props {
17
+ courseProductRelationIds?: CourseProductRelation['id'][];
17
18
  organizationId: Organization['id'];
18
19
  contractToSignCount: number;
19
20
  }
20
21
 
21
- const SignOrganizationContractButton = ({ organizationId, contractToSignCount }: Props) => {
22
+ const SignOrganizationContractButton = ({
23
+ organizationId,
24
+ contractToSignCount,
25
+ courseProductRelationIds = [],
26
+ }: Props) => {
22
27
  const [contractFrameOpened, setContractFrameOpened] = useState(false);
23
28
  const hasContractToSign = contractToSignCount > 0;
24
29
 
@@ -43,6 +48,7 @@ const SignOrganizationContractButton = ({ organizationId, contractToSignCount }:
43
48
  </Button>
44
49
  )}
45
50
  <OrganizationContractFrame
51
+ courseProductRelationIds={courseProductRelationIds}
46
52
  organizationId={organizationId}
47
53
  isOpen={contractFrameOpened}
48
54
  onClose={() => setContractFrameOpened(false)}
@@ -452,6 +452,7 @@ export interface ContractResourceQuery extends PaginatedResourceQuery {
452
452
  export interface OrganizationContractSignatureLinksFilters {
453
453
  contracts_ids?: string[];
454
454
  organization_id: Organization['id'];
455
+ course_product_relation_ids?: CourseProductRelation['id'][];
455
456
  }
456
457
 
457
458
  export interface ContractInvitationLinkResponse {
@@ -0,0 +1,76 @@
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+ import useDateRelative from 'hooks/useDateRelative';
3
+ import { Priority } from 'types';
4
+ import { CourseRun } from 'types/Joanie';
5
+ import useDateFormat, { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
6
+
7
+ const messages = defineMessages({
8
+ onGoingRunPeriod: {
9
+ id: 'components.useCourseRunPeriodMessage.onGoingRunPeriod',
10
+ description: 'Text to display when a course run is on going.',
11
+ defaultMessage: 'This session started on {startDate} and will end on {endDate}',
12
+ },
13
+ futureRunPeriod: {
14
+ id: 'components.useCourseRunPeriodMessage.futureRunPeriod',
15
+ description: 'Text to display the period of a future course run.',
16
+ defaultMessage: 'This session starts {relativeStartDate}, the {startDate}',
17
+ },
18
+ onGoingEnrolledRunPeriod: {
19
+ id: 'components.useCourseRunPeriodMessage.onGoingEnrolledRunPeriod',
20
+ description: 'Text to display when a course run is ongoing and the user is enrolled to.',
21
+ defaultMessage: "You are enrolled for this session. It's open from {startDate} to {endDate}",
22
+ },
23
+ futureEnrolledRunPeriod: {
24
+ id: 'components.useCourseRunPeriodMessage.futureEnrolledRunPeriod',
25
+ description: 'Text to display when a course run is not yet opened and the user is enrolled to.',
26
+ defaultMessage:
27
+ 'You are enrolled for this session. It starts {relativeStartDate}, the {startDate}.',
28
+ },
29
+ archivedEnrolledRunPeriod: {
30
+ id: 'components.useCourseRunPeriodMessage.archivedEnrolledRunPeriod',
31
+ description: 'Text to display when a course run is archived and the user is enrolled to.',
32
+ defaultMessage: 'You are enrolled for this session.',
33
+ },
34
+ });
35
+
36
+ const useCourseRunPeriodMessage = (courseRun: CourseRun, enrolled: boolean = false) => {
37
+ const intl = useIntl();
38
+ const formatDate = useDateFormat();
39
+
40
+ const relativeStartDate = useDateRelative(new Date(courseRun.start));
41
+ const startDate = formatDate(courseRun.start, DEFAULT_DATE_FORMAT);
42
+ const endDate = formatDate(courseRun.end, DEFAULT_DATE_FORMAT);
43
+ const isArchived = [Priority.ARCHIVED_CLOSED, Priority.ARCHIVED_OPEN].includes(
44
+ courseRun.state.priority,
45
+ );
46
+ const isOnGoing = [Priority.ONGOING_OPEN, Priority.ONGOING_CLOSED].includes(
47
+ courseRun.state.priority,
48
+ );
49
+ if (enrolled) {
50
+ if (isArchived) {
51
+ return intl.formatMessage(messages.archivedEnrolledRunPeriod);
52
+ }
53
+ if (isOnGoing) {
54
+ return intl.formatMessage(messages.onGoingEnrolledRunPeriod, {
55
+ startDate,
56
+ endDate,
57
+ });
58
+ }
59
+ return intl.formatMessage(messages.futureEnrolledRunPeriod, {
60
+ relativeStartDate,
61
+ startDate,
62
+ });
63
+ }
64
+ if (isOnGoing) {
65
+ return intl.formatMessage(messages.onGoingRunPeriod, {
66
+ startDate,
67
+ endDate,
68
+ });
69
+ }
70
+ return intl.formatMessage(messages.futureRunPeriod, {
71
+ relativeStartDate,
72
+ startDate,
73
+ });
74
+ };
75
+
76
+ export default useCourseRunPeriodMessage;