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.
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +95 -129
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +10 -2
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +37 -6
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +11 -2
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +2 -0
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +50 -9
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +8 -2
- package/js/types/Joanie.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.spec.tsx → CourseEnrolling/index.spec.tsx} +44 -13
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +38 -57
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +7 -19
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +76 -24
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +3 -1
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +136 -30
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +60 -12
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -2
- 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 {
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 = ({
|
|
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
|
|
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/`,
|
|
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/${
|
|
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/${
|
|
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.
|
|
105
|
+
const organizationFilter: HTMLInputElement = await screen.findByRole('combobox', {
|
|
105
106
|
name: 'Organization',
|
|
106
107
|
});
|
|
107
|
-
expect(organizationFilter).toHaveAttribute('value',
|
|
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={!!
|
|
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
|
-
|
|
39
|
+
fetchMock.restore();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
it(
|
|
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={
|
|
47
|
+
contractToSignCount={12}
|
|
47
48
|
/>
|
|
48
49
|
</Wrapper>,
|
|
49
50
|
);
|
|
50
51
|
|
|
51
|
-
expect(
|
|
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
|
|
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
|
});
|
package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx
CHANGED
|
@@ -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 = ({
|
|
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)}
|
package/js/types/Joanie.ts
CHANGED
|
@@ -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;
|