richie-education 3.2.1-dev9 → 3.2.2-dev26

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 (98) hide show
  1. package/i18n/locales/ar-SA.json +29 -1
  2. package/i18n/locales/es-ES.json +29 -1
  3. package/i18n/locales/fa-IR.json +29 -1
  4. package/i18n/locales/fr-CA.json +29 -1
  5. package/i18n/locales/fr-FR.json +29 -1
  6. package/i18n/locales/ko-KR.json +29 -1
  7. package/i18n/locales/pt-PT.json +29 -1
  8. package/i18n/locales/ru-RU.json +29 -1
  9. package/i18n/locales/vi-VN.json +29 -1
  10. package/js/api/joanie.ts +144 -0
  11. package/js/components/PaymentInterfaces/types.ts +7 -0
  12. package/js/components/PaymentScheduleGrid/index.tsx +4 -2
  13. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +9 -2
  14. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +33 -0
  15. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +253 -0
  16. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +314 -0
  17. package/js/components/SaleTunnel/SaleTunnelInformation/StepContent.tsx +528 -0
  18. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +47 -261
  19. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +25 -11
  20. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +54 -6
  21. package/js/components/SaleTunnel/_styles.scss +55 -0
  22. package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +356 -0
  23. package/js/components/SaleTunnel/{index.full-process.spec.tsx → index.full-process-b2c.spec.tsx} +4 -1
  24. package/js/components/SaleTunnel/index.spec.tsx +130 -1
  25. package/js/hooks/useBatchOrder/index.tsx +36 -0
  26. package/js/hooks/useContractArchive/index.ts +2 -0
  27. package/js/hooks/useOfferingOrganizations/index.tsx +38 -0
  28. package/js/hooks/useOrganizationAgreements.tsx/index.tsx +66 -0
  29. package/js/hooks/useOrganizationQuotes/index.tsx +56 -0
  30. package/js/hooks/usePaymentPlan.tsx +2 -1
  31. package/js/hooks/useTeacherPendingAgreementsCount/index.ts +34 -0
  32. package/js/pages/DashboardBatchOrderLayout/_styles.scss +5 -0
  33. package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +78 -0
  34. package/js/pages/DashboardBatchOrderLayout/index.tsx +45 -0
  35. package/js/pages/DashboardBatchOrders/index.spec.tsx +237 -0
  36. package/js/pages/DashboardBatchOrders/index.tsx +84 -0
  37. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardCourseContractsLayout/index.tsx +0 -1
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +3 -1
  39. package/js/pages/TeacherDashboardOrganizationAgreements/AgreementActionsBar.tsx +49 -0
  40. package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +79 -0
  41. package/js/pages/TeacherDashboardOrganizationAgreements/OrganizationAgreementFrame.tsx +71 -0
  42. package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +60 -0
  43. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useAgreementsAbilities.tsx +8 -0
  44. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useHasAgreementToDownload.tsx +27 -0
  45. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useTeacherAgreementsToSign.tsx +32 -0
  46. package/js/pages/TeacherDashboardOrganizationAgreements/index.spec.tsx +433 -0
  47. package/js/pages/TeacherDashboardOrganizationAgreements/index.tsx +130 -0
  48. package/js/pages/TeacherDashboardOrganizationAgreementsLayout/index.tsx +25 -0
  49. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +9 -0
  50. package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +40 -0
  51. package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +194 -0
  52. package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +144 -0
  53. package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +521 -0
  54. package/js/pages/TeacherDashboardOrganizationQuotesLayout/index.tsx +26 -0
  55. package/js/translations/ar-SA.json +1 -1
  56. package/js/translations/es-ES.json +1 -1
  57. package/js/translations/fa-IR.json +1 -1
  58. package/js/translations/fr-CA.json +1 -1
  59. package/js/translations/fr-FR.json +1 -1
  60. package/js/translations/ko-KR.json +1 -1
  61. package/js/translations/pt-PT.json +1 -1
  62. package/js/translations/ru-RU.json +1 -1
  63. package/js/translations/vi-VN.json +1 -1
  64. package/js/types/Joanie.ts +216 -1
  65. package/js/utils/AbilitiesHelper/agreementAbilities.ts +14 -0
  66. package/js/utils/AbilitiesHelper/index.ts +7 -0
  67. package/js/utils/AbilitiesHelper/types.ts +12 -3
  68. package/js/utils/ObjectHelper/index.ts +20 -0
  69. package/js/utils/OrderHelper/index.ts +10 -0
  70. package/js/utils/errors/HttpError.ts +1 -0
  71. package/js/utils/test/factories/joanie.ts +156 -1
  72. package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/_styles.scss +14 -0
  73. package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/index.tsx +32 -0
  74. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +18 -0
  75. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +25 -2
  76. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +4 -2
  77. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +88 -0
  78. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +216 -0
  79. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +316 -0
  80. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.spec.tsx +27 -0
  81. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +175 -0
  82. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +5 -2
  83. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +4 -1
  84. package/js/widgets/Dashboard/components/DashboardItem/Order/_styles.scss +5 -0
  85. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +43 -0
  86. package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.spec.tsx +214 -0
  87. package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.tsx +47 -0
  88. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +1 -0
  89. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +21 -3
  90. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +9 -0
  91. package/js/widgets/Dashboard/utils/learnerRoutes.tsx +30 -0
  92. package/js/widgets/Dashboard/utils/learnerRoutesPaths.tsx +12 -0
  93. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +12 -0
  94. package/js/widgets/Dashboard/utils/teacherRoutes.tsx +17 -0
  95. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +8 -2
  96. package/package.json +4 -1
  97. package/scss/colors/_theme.scss +1 -1
  98. package/scss/components/_index.scss +1 -0
@@ -0,0 +1,433 @@
1
+ import fetchMock from 'fetch-mock';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import userEvent, { UserEvent } from '@testing-library/user-event';
4
+ import { browserDownloadFromBlob } from 'utils/download';
5
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
6
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
7
+ import { render } from 'utils/test/render';
8
+ import { expectNoSpinner } from 'utils/test/expectSpinner';
9
+ import { AgreementFactory } from 'utils/test/factories/joanie';
10
+ import { expectBannerError } from 'utils/test/expectBanner';
11
+ import TeacherDashboardOrganizationAgreements from '.';
12
+
13
+ let user: UserEvent;
14
+
15
+ jest.mock('utils/context', () => ({
16
+ __esModule: true,
17
+ default: mockRichieContextFactory({
18
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.endpoint.test' },
19
+ joanie_backend: { endpoint: 'https://joanie.endpoint' },
20
+ }).one(),
21
+ }));
22
+
23
+ jest.mock('utils/download', () => ({
24
+ browserDownloadFromBlob: jest.fn((fn) => fn().then(() => true)),
25
+ }));
26
+
27
+ describe('pages/TeacherDashboardOrganizationAgreements', () => {
28
+ beforeEach(() => {
29
+ user = userEvent.setup();
30
+ jest.resetAllMocks();
31
+ });
32
+ setupJoanieSession();
33
+
34
+ it('should render a list of agreements for an organization', async () => {
35
+ const agreementList = AgreementFactory().many(5);
36
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
37
+ fetchMock.get(
38
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
39
+ {
40
+ results: [],
41
+ count: 0,
42
+ previous: null,
43
+ next: null,
44
+ },
45
+ );
46
+ fetchMock.get(
47
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
48
+ {
49
+ results: agreementList,
50
+ count: 0,
51
+ previous: null,
52
+ next: null,
53
+ },
54
+ );
55
+ fetchMock.get(
56
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
57
+ {
58
+ results: [],
59
+ count: 0,
60
+ previous: null,
61
+ next: null,
62
+ },
63
+ );
64
+ fetchMock.get(
65
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
66
+ {
67
+ results: [],
68
+ count: 0,
69
+ previous: null,
70
+ next: null,
71
+ },
72
+ );
73
+ render(<TeacherDashboardOrganizationAgreements />, {
74
+ routerOptions: {
75
+ path: '/organizations/:organizationId/agreements',
76
+ initialEntries: ['/organizations/1/agreements'],
77
+ },
78
+ });
79
+
80
+ await expectNoSpinner();
81
+ agreementList.forEach((agreement) => {
82
+ expect(screen.getByTestId(agreement.id));
83
+ });
84
+ });
85
+
86
+ it('should render an empty list of agreements for an organization', async () => {
87
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
88
+ fetchMock.get(
89
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
90
+ {
91
+ results: [],
92
+ count: 0,
93
+ previous: null,
94
+ next: null,
95
+ },
96
+ );
97
+ fetchMock.get(
98
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
99
+ {
100
+ results: [],
101
+ count: 0,
102
+ previous: null,
103
+ next: null,
104
+ },
105
+ );
106
+ fetchMock.get(
107
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
108
+ {
109
+ results: [],
110
+ count: 0,
111
+ previous: null,
112
+ next: null,
113
+ },
114
+ );
115
+
116
+ render(<TeacherDashboardOrganizationAgreements />, {
117
+ routerOptions: {
118
+ path: '/organizations/:organizationId/agreements',
119
+ initialEntries: ['/organizations/1/agreements'],
120
+ },
121
+ });
122
+ await expectNoSpinner();
123
+ expect(screen.getByRole('img', { name: /illustration of an empty table/i }));
124
+ });
125
+
126
+ it('should display an error when API fails', async () => {
127
+ fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', []);
128
+ fetchMock.get(
129
+ 'https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed',
130
+ 500,
131
+ );
132
+ fetchMock.get(
133
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
134
+ 500,
135
+ );
136
+ fetchMock.get(
137
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
138
+ 500,
139
+ );
140
+
141
+ render(<TeacherDashboardOrganizationAgreements />, {
142
+ routerOptions: {
143
+ path: '/organizations/:organizationId/agreements',
144
+ initialEntries: ['/organizations/1/agreements'],
145
+ },
146
+ });
147
+
148
+ await expectNoSpinner();
149
+ expectBannerError('An error occurred while fetching contracts. Please retry later');
150
+ });
151
+
152
+ it('should paginate', async () => {
153
+ const agreementList = AgreementFactory().many(30);
154
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
155
+ fetchMock.get(
156
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
157
+ {
158
+ results: [],
159
+ count: 0,
160
+ previous: null,
161
+ next: null,
162
+ },
163
+ );
164
+ fetchMock.get(
165
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
166
+ {
167
+ results: [],
168
+ count: 0,
169
+ previous: null,
170
+ next: null,
171
+ },
172
+ );
173
+
174
+ fetchMock.get(
175
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
176
+ {
177
+ results: agreementList.slice(0, 25),
178
+ count: 30,
179
+ previous: null,
180
+ next: 'https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=2&page_size=25',
181
+ },
182
+ );
183
+
184
+ fetchMock.get(
185
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=2&page_size=25`,
186
+ {
187
+ results: agreementList.slice(25, 30),
188
+ count: 30,
189
+ previous:
190
+ 'https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25',
191
+ next: null,
192
+ },
193
+ );
194
+
195
+ render(<TeacherDashboardOrganizationAgreements />, {
196
+ routerOptions: {
197
+ path: '/organizations/:organizationId/agreements',
198
+ initialEntries: ['/organizations/1/agreements'],
199
+ },
200
+ });
201
+
202
+ await expectNoSpinner();
203
+
204
+ expect(screen.getByText(agreementList[0].batch_order.owner_name)).toBeInTheDocument();
205
+ expect(screen.queryByText(agreementList[29].batch_order.owner_name)).not.toBeInTheDocument();
206
+
207
+ const nextButton = screen.getByRole('button', { name: /next page/i });
208
+ await user.click(nextButton);
209
+
210
+ await waitFor(() => {
211
+ expect(screen.getByText(agreementList[29].batch_order.owner_name)).toBeInTheDocument();
212
+ });
213
+ expect(screen.queryByText(agreementList[0].batch_order.owner_name)).not.toBeInTheDocument();
214
+ });
215
+
216
+ it('should filter by signature state', async () => {
217
+ const agreementList = AgreementFactory().many(5);
218
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
219
+
220
+ fetchMock.get(
221
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
222
+ {
223
+ results: agreementList,
224
+ count: 5,
225
+ previous: null,
226
+ next: null,
227
+ },
228
+ );
229
+ fetchMock.get(
230
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
231
+ {
232
+ results: [],
233
+ count: 0,
234
+ previous: null,
235
+ next: null,
236
+ },
237
+ );
238
+
239
+ fetchMock.get(
240
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
241
+ {
242
+ results: [],
243
+ count: 0,
244
+ previous: null,
245
+ next: null,
246
+ },
247
+ );
248
+ fetchMock.get(
249
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=half_signed&page=1&page_size=25`,
250
+ {
251
+ results: [],
252
+ count: 0,
253
+ previous: null,
254
+ next: null,
255
+ },
256
+ );
257
+ fetchMock.get(
258
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
259
+ {
260
+ results: [],
261
+ count: 0,
262
+ previous: null,
263
+ next: null,
264
+ },
265
+ );
266
+
267
+ render(<TeacherDashboardOrganizationAgreements />, {
268
+ routerOptions: {
269
+ path: '/organizations/:organizationId/agreements',
270
+ initialEntries: ['/organizations/1/agreements'],
271
+ },
272
+ });
273
+
274
+ await expectNoSpinner();
275
+
276
+ expect(screen.getByText(agreementList[0].batch_order.owner_name)).toBeInTheDocument();
277
+
278
+ const signatureStateCombobox = screen.getByRole('combobox', { name: /signature state/i });
279
+ await user.click(signatureStateCombobox);
280
+ const pendingOption = screen.getByRole('option', { name: /Pending for signature/i });
281
+ await user.click(pendingOption);
282
+
283
+ await waitFor(() => {
284
+ expect(
285
+ fetchMock.called(
286
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
287
+ ),
288
+ ).toBe(true);
289
+ });
290
+ });
291
+
292
+ it('should allow to sign agreements', async () => {
293
+ const agreementList = AgreementFactory()
294
+ .many(5)
295
+ .map((agreement: any) => ({ ...agreement, abilities: { sign: true } }));
296
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
297
+
298
+ fetchMock.get(
299
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
300
+ {
301
+ results: agreementList,
302
+ count: 5,
303
+ previous: null,
304
+ next: null,
305
+ },
306
+ );
307
+ fetchMock.get(
308
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
309
+ { results: [], count: 0, previous: null, next: null },
310
+ );
311
+ fetchMock.get(
312
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=half_signed&page=1&page_size=25`,
313
+ { results: [], count: 0, previous: null, next: null },
314
+ );
315
+ fetchMock.get(
316
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
317
+ { results: [], count: 0, previous: null, next: null },
318
+ );
319
+ fetchMock.get(
320
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
321
+ {
322
+ results: agreementList,
323
+ count: 5,
324
+ previous: null,
325
+ next: null,
326
+ },
327
+ );
328
+
329
+ render(<TeacherDashboardOrganizationAgreements />, {
330
+ routerOptions: {
331
+ path: '/organizations/:organizationId/agreements',
332
+ initialEntries: ['/organizations/1/agreements'],
333
+ },
334
+ });
335
+
336
+ await expectNoSpinner();
337
+
338
+ const signatureStateCombobox = screen.getByRole('combobox', { name: /signature state/i });
339
+ await user.click(signatureStateCombobox);
340
+ const pendingOption = screen.getByRole('option', { name: /Pending for signature/i });
341
+ await user.click(pendingOption);
342
+
343
+ const signButton = await screen.findByRole('button', { name: /Sign all pending agreements/i });
344
+ expect(signButton).toBeInTheDocument();
345
+
346
+ fetchMock.get(
347
+ 'https://joanie.endpoint/api/v1.0/organizations/1/contracts-signature-link/?from_batch_order=true',
348
+ {
349
+ invitation_link: 'https://dummysignaturebackend.fr',
350
+ contract_ids: agreementList.map((a) => a.id),
351
+ },
352
+ );
353
+
354
+ await user.click(signButton);
355
+ expect(await screen.findByTestId('dashboard-contract-frame')).toBeInTheDocument();
356
+ });
357
+
358
+ it('should download agreement archive', async () => {
359
+ const agreementList = AgreementFactory()
360
+ .many(5)
361
+ .map((agreement: any) => ({ ...agreement, abilities: { sign: true } }));
362
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
363
+ fetchMock.get(
364
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed&page=1&page_size=25`,
365
+ { results: [], count: 0, previous: null, next: null },
366
+ );
367
+ fetchMock.get(
368
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
369
+ { results: [], count: 0, previous: null, next: null },
370
+ );
371
+ fetchMock.get(
372
+ `https://joanie.endpoint/api/v1.0/organizations/1/contracts/?signature_state=half_signed&page=1&page_size=25`,
373
+ { results: [], count: 0, previous: null, next: null },
374
+ );
375
+ fetchMock.get(
376
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=half_signed`,
377
+ {
378
+ results: [],
379
+ count: 0,
380
+ previous: null,
381
+ next: null,
382
+ },
383
+ );
384
+ fetchMock.get(
385
+ `https://joanie.endpoint/api/v1.0/organizations/1/agreements/?signature_state=signed&page=1&page_size=25`,
386
+ {
387
+ results: agreementList,
388
+ count: 5,
389
+ previous: null,
390
+ next: null,
391
+ },
392
+ );
393
+
394
+ render(<TeacherDashboardOrganizationAgreements />, {
395
+ routerOptions: {
396
+ path: '/organizations/:organizationId/agreements',
397
+ initialEntries: ['/organizations/1/agreements'],
398
+ },
399
+ });
400
+
401
+ await expectNoSpinner();
402
+
403
+ const downloadButton = await screen.findByRole('button', {
404
+ name: /Request contracts archive/i,
405
+ });
406
+ expect(downloadButton).toBeInTheDocument();
407
+ expect(downloadButton).toBeEnabled();
408
+
409
+ const archiveId = '85e098fd-4375-4e0d-9596-3c58988647D0';
410
+ fetchMock.post('https://joanie.endpoint/api/v1.0/contracts/zip-archive/', {
411
+ url: `https://joanie.endpoint/api/v1.0/contracts/zip-archive/${archiveId}/`,
412
+ });
413
+
414
+ fetchMock.mock(`https://joanie.endpoint/api/v1.0/contracts/zip-archive/${archiveId}/`, 204, {
415
+ method: 'OPTIONS',
416
+ });
417
+
418
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/contracts/zip-archive/${archiveId}/`, {
419
+ status: 200,
420
+ body: new Blob(['test']),
421
+ });
422
+
423
+ await waitFor(() => {
424
+ expect(screen.getByRole('button', { name: /Request contracts archive/i })).toBeEnabled();
425
+ });
426
+
427
+ await user.click(downloadButton);
428
+
429
+ await waitFor(() => {
430
+ expect(browserDownloadFromBlob).toHaveBeenCalledTimes(1);
431
+ });
432
+ });
433
+ });
@@ -0,0 +1,130 @@
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+
3
+ import { DataGrid, usePagination } from '@openfun/cunningham-react';
4
+ import { useEffect, useMemo } from 'react';
5
+ import { useParams, useSearchParams } from 'react-router';
6
+ import Banner, { BannerType } from 'components/Banner';
7
+ import { PER_PAGE } from 'settings';
8
+ import { ContractResourceQuery } from 'types/Joanie';
9
+
10
+ import { useOrganizations } from 'hooks/useOrganizations';
11
+ import useTeacherContractFilters, {
12
+ TeacherDashboardContractsParams,
13
+ } from 'pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters';
14
+ import ContractFiltersBar from 'pages/TeacherDashboardContractsLayout/components/ContractFiltersBar';
15
+ import { useOrganizationAgreements } from 'hooks/useOrganizationAgreements.tsx';
16
+ import AgreementActionsBar from './AgreementActionsBar';
17
+
18
+ const messages = defineMessages({
19
+ columnProductTitle: {
20
+ defaultMessage: 'Training',
21
+ description: 'Label for productTitle column',
22
+ id: 'pages.TeacherDashboardOrganizationContractsLayout.columnProductTitle',
23
+ },
24
+ columnLearnerName: {
25
+ defaultMessage: 'Learner',
26
+ description: 'Label for learnerName column',
27
+ id: 'pages.TeacherDashboardOrganizationContractsLayout.columnLearnerName',
28
+ },
29
+ columnState: {
30
+ defaultMessage: 'State',
31
+ description: 'Label for state column',
32
+ id: 'pages.TeacherDashboardOrganizationContractsLayout.columnState',
33
+ },
34
+ });
35
+
36
+ const TeacherDashboardOrganizationAgreements = () => {
37
+ const intl = useIntl();
38
+ const [searchParams] = useSearchParams();
39
+ const page = searchParams.get('page') ?? '1';
40
+ const pagination = usePagination({
41
+ defaultPage: page ? parseInt(page, 10) : 1,
42
+ pageSize: PER_PAGE.teacherContractList,
43
+ });
44
+ const { organizationId: routeOrganizationId, offeringId: routeOfferingId } =
45
+ 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({ offering_id: routeOfferingId }, { enabled: !routeOrganizationId });
53
+ const hasMultipleOrganizations = isOrganizationListFetched && organizationList.length > 1;
54
+ const { initialFilters, filters, setFilters } = useTeacherContractFilters();
55
+ const {
56
+ items: agreements,
57
+ meta,
58
+ states: { fetching, isFetched, error },
59
+ } = useOrganizationAgreements({
60
+ ...filters,
61
+ page: pagination.page,
62
+ page_size: PER_PAGE.teacherContractList,
63
+ });
64
+
65
+ const rows = useMemo(() => {
66
+ return agreements.map((agreement) => ({
67
+ id: agreement.id,
68
+ learnerName: agreement.batch_order.owner_name,
69
+ productTitle: agreement.batch_order.relation.product.title,
70
+ state: agreement.batch_order.state,
71
+ }));
72
+ }, [agreements]);
73
+
74
+ const handleFiltersChange = (newFilters: Partial<ContractResourceQuery>) => {
75
+ // Reset pagination
76
+ pagination.setPage(1);
77
+ setFilters((prevFilters: any) => ({ ...prevFilters, ...newFilters }));
78
+ };
79
+
80
+ useEffect(() => {
81
+ if (isFetched && meta?.pagination?.count) {
82
+ pagination.setPagesCount(Math.ceil(meta!.pagination!.count / PER_PAGE.teacherContractList));
83
+ }
84
+ }, [meta, isFetched]);
85
+
86
+ if (error) {
87
+ return <Banner message={error} type={BannerType.ERROR} rounded />;
88
+ }
89
+
90
+ return (
91
+ <div className="teacher-contract-page">
92
+ <div className="dashboard__page__actions">
93
+ <AgreementActionsBar
94
+ organizationId={filters.organization_id!}
95
+ offeringId={filters.offering_id}
96
+ />
97
+ <ContractFiltersBar
98
+ defaultValues={initialFilters}
99
+ onFiltersChange={handleFiltersChange}
100
+ organizationList={organizationList}
101
+ hideFilterOrganization={!!(routeOrganizationId || !hasMultipleOrganizations)}
102
+ />
103
+ </div>
104
+ <DataGrid
105
+ columns={[
106
+ {
107
+ field: 'productTitle',
108
+ headerName: intl.formatMessage(messages.columnProductTitle),
109
+ enableSorting: false,
110
+ },
111
+ {
112
+ field: 'learnerName',
113
+ headerName: intl.formatMessage(messages.columnLearnerName),
114
+ enableSorting: false,
115
+ },
116
+ {
117
+ field: 'state',
118
+ headerName: intl.formatMessage(messages.columnState),
119
+ enableSorting: false,
120
+ },
121
+ ]}
122
+ rows={rows}
123
+ pagination={pagination}
124
+ isLoading={fetching}
125
+ />
126
+ </div>
127
+ );
128
+ };
129
+
130
+ export default TeacherDashboardOrganizationAgreements;
@@ -0,0 +1,25 @@
1
+ import { defineMessages, FormattedMessage } from 'react-intl';
2
+ import TeacherDashboardOrganizationAgreements from 'pages/TeacherDashboardOrganizationAgreements';
3
+ import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
4
+ import { TeacherDashboardOrganizationSidebar } from 'widgets/Dashboard/components/TeacherDashboardOrganizationSidebar';
5
+
6
+ const messages = defineMessages({
7
+ pageTitle: {
8
+ defaultMessage: 'Agreements',
9
+ description: 'Use for the page title of the organization agreements',
10
+ id: 'pages.TeacherDashboardOrganizationAgreementsLayout.pageTitle',
11
+ },
12
+ });
13
+
14
+ export const TeacherDashboardOrganizationAgreementsLayout = () => {
15
+ return (
16
+ <DashboardLayout sidebar={<TeacherDashboardOrganizationSidebar />}>
17
+ <div className="dashboard__page_title_container">
18
+ <h1 className="dashboard__page_title">
19
+ <FormattedMessage {...messages.pageTitle} />
20
+ </h1>
21
+ </div>
22
+ <TeacherDashboardOrganizationAgreements />
23
+ </DashboardLayout>
24
+ );
25
+ };
@@ -66,6 +66,10 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
66
66
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&page=1`,
67
67
  [],
68
68
  );
69
+ fetchMock.get(
70
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/agreements/?signature_state=half_signed&page=1`,
71
+ [],
72
+ );
69
73
 
70
74
  render(<TeacherDashboardOrganizationCourseLoader />, {
71
75
  routerOptions: {
@@ -78,6 +82,7 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
78
82
  nbApiCalls += 1; // course api call
79
83
  nbApiCalls += 1; // offerings api call
80
84
  nbApiCalls += 1; // contracts api call
85
+ nbApiCalls += 1; // agreements api call
81
86
  const calledUrls = fetchMock.calls().map((call) => call[0]);
82
87
  expect(calledUrls).toHaveLength(nbApiCalls);
83
88
  expect(calledUrls).toContain(
@@ -105,6 +110,10 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
105
110
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&page=1`,
106
111
  [],
107
112
  );
113
+ fetchMock.get(
114
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/agreements/?signature_state=half_signed&page=1`,
115
+ [],
116
+ );
108
117
  nbApiCalls += 1;
109
118
  fetchMock.get(
110
119
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/`,
@@ -0,0 +1,40 @@
1
+ .dashboard__quotes {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 1rem;
5
+ }
6
+
7
+ .dashboard__quote__header {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 0.5rem;
11
+ justify-content: start;
12
+ &__main {
13
+ display: flex;
14
+ flex-direction: row;
15
+ justify-content: start;
16
+ gap: 0.5rem;
17
+ }
18
+ &__action {
19
+ gap: 0.5rem;
20
+ }
21
+ }
22
+
23
+ .dashboard__quote__informations {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 0.5rem;
27
+ }
28
+
29
+ .dashboard__quote__reference,
30
+ .dashboard__quote__information {
31
+ display: flex;
32
+ flex-direction: row;
33
+ align-items: center;
34
+ gap: 0.2rem;
35
+ }
36
+
37
+ .dashboard__quote__modal {
38
+ display: flex;
39
+ justify-content: center;
40
+ }