richie-education 3.2.1 → 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 (78) hide show
  1. package/js/api/joanie.ts +144 -0
  2. package/js/components/PaymentInterfaces/types.ts +7 -0
  3. package/js/components/PaymentScheduleGrid/index.tsx +4 -2
  4. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +9 -2
  5. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +33 -0
  6. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +253 -0
  7. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +314 -0
  8. package/js/components/SaleTunnel/SaleTunnelInformation/StepContent.tsx +528 -0
  9. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +47 -271
  10. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +25 -11
  11. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +54 -6
  12. package/js/components/SaleTunnel/_styles.scss +55 -0
  13. package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +356 -0
  14. package/js/components/SaleTunnel/{index.full-process.spec.tsx → index.full-process-b2c.spec.tsx} +4 -1
  15. package/js/components/SaleTunnel/index.spec.tsx +104 -0
  16. package/js/hooks/useBatchOrder/index.tsx +36 -0
  17. package/js/hooks/useContractArchive/index.ts +2 -0
  18. package/js/hooks/useOfferingOrganizations/index.tsx +38 -0
  19. package/js/hooks/useOrganizationAgreements.tsx/index.tsx +66 -0
  20. package/js/hooks/useOrganizationQuotes/index.tsx +56 -0
  21. package/js/hooks/useTeacherPendingAgreementsCount/index.ts +34 -0
  22. package/js/pages/DashboardBatchOrderLayout/_styles.scss +5 -0
  23. package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +78 -0
  24. package/js/pages/DashboardBatchOrderLayout/index.tsx +45 -0
  25. package/js/pages/DashboardBatchOrders/index.spec.tsx +237 -0
  26. package/js/pages/DashboardBatchOrders/index.tsx +84 -0
  27. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardCourseContractsLayout/index.tsx +0 -1
  28. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +3 -1
  29. package/js/pages/TeacherDashboardOrganizationAgreements/AgreementActionsBar.tsx +49 -0
  30. package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +79 -0
  31. package/js/pages/TeacherDashboardOrganizationAgreements/OrganizationAgreementFrame.tsx +71 -0
  32. package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +60 -0
  33. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useAgreementsAbilities.tsx +8 -0
  34. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useHasAgreementToDownload.tsx +27 -0
  35. package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useTeacherAgreementsToSign.tsx +32 -0
  36. package/js/pages/TeacherDashboardOrganizationAgreements/index.spec.tsx +433 -0
  37. package/js/pages/TeacherDashboardOrganizationAgreements/index.tsx +130 -0
  38. package/js/pages/TeacherDashboardOrganizationAgreementsLayout/index.tsx +25 -0
  39. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +9 -0
  40. package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +40 -0
  41. package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +194 -0
  42. package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +144 -0
  43. package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +521 -0
  44. package/js/pages/TeacherDashboardOrganizationQuotesLayout/index.tsx +26 -0
  45. package/js/types/Joanie.ts +216 -1
  46. package/js/utils/AbilitiesHelper/agreementAbilities.ts +14 -0
  47. package/js/utils/AbilitiesHelper/index.ts +7 -0
  48. package/js/utils/AbilitiesHelper/types.ts +12 -3
  49. package/js/utils/ObjectHelper/index.ts +20 -0
  50. package/js/utils/OrderHelper/index.ts +10 -0
  51. package/js/utils/test/factories/joanie.ts +156 -1
  52. package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/_styles.scss +14 -0
  53. package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/index.tsx +32 -0
  54. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +18 -0
  55. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +25 -2
  56. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +4 -2
  57. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +88 -0
  58. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +216 -0
  59. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +316 -0
  60. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.spec.tsx +27 -0
  61. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +175 -0
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +5 -2
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +4 -1
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/_styles.scss +5 -0
  65. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +43 -0
  66. package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.spec.tsx +214 -0
  67. package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.tsx +47 -0
  68. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +1 -0
  69. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +21 -3
  70. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +9 -0
  71. package/js/widgets/Dashboard/utils/learnerRoutes.tsx +30 -0
  72. package/js/widgets/Dashboard/utils/learnerRoutesPaths.tsx +12 -0
  73. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +12 -0
  74. package/js/widgets/Dashboard/utils/teacherRoutes.tsx +17 -0
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +8 -2
  76. package/package.json +4 -1
  77. package/scss/colors/_theme.scss +1 -1
  78. package/scss/components/_index.scss +1 -0
package/js/api/joanie.ts CHANGED
@@ -101,6 +101,13 @@ export const getRoutes = () => {
101
101
  submit_installment_payment: `${baseUrl}/orders/:id/submit-installment-payment/`,
102
102
  set_payment_method: `${baseUrl}/orders/:id/payment-method/`,
103
103
  },
104
+ batchOrders: {
105
+ get: `${baseUrl}/batch-orders/:id/`,
106
+ create: `${baseUrl}/batch-orders/`,
107
+ submit_for_payment: {
108
+ create: `${baseUrl}/batch-orders/:id/submit-for-payment/`,
109
+ },
110
+ },
104
111
  certificates: {
105
112
  download: `${baseUrl}/certificates/:id/download/`,
106
113
  get: `${baseUrl}/certificates/:id/`,
@@ -136,6 +143,25 @@ export const getRoutes = () => {
136
143
  get: `${baseUrl}/organizations/:organization_id/contracts/:id/`,
137
144
  getSignatureLinks: `${baseUrl}/organizations/:organization_id/contracts-signature-link/`,
138
145
  },
146
+ quotes: {
147
+ get: `${baseUrl}/organizations/:organization_id/quotes/:id/`,
148
+ update: `${baseUrl}/organizations/:organization_id/confirm-quote/`,
149
+ purchase_order: {
150
+ update: `${baseUrl}/organizations/:organization_id/confirm-purchase-order/`,
151
+ },
152
+ bank_transfer: {
153
+ create: `${baseUrl}/organizations/:organization_id/confirm-bank-transfer/`,
154
+ },
155
+ submit_for_signature: {
156
+ create: `${baseUrl}/organizations/:organization_id/submit-for-signature-batch-order/`,
157
+ },
158
+ download_quote: {
159
+ get: `${baseUrl}/organizations/:organization_id/download-quote/`,
160
+ },
161
+ },
162
+ agreements: {
163
+ get: `${baseUrl}/organizations/:organization_id/agreements/:id/`,
164
+ },
139
165
  },
140
166
  courses: {
141
167
  get: `${baseUrl}/courses/:id/`,
@@ -157,6 +183,9 @@ export const getRoutes = () => {
157
183
  },
158
184
  offerings: {
159
185
  get: `${baseUrl}/offerings/:id/`,
186
+ organizations: {
187
+ get: `${baseUrl}/offerings/:id/get-organizations/`,
188
+ },
160
189
  },
161
190
  contractDefinitions: {
162
191
  previewTemplate: `${baseUrl}/contract_definitions/:id/preview_template/`,
@@ -303,6 +332,26 @@ const API = (): Joanie.API => {
303
332
  body: JSON.stringify(payload),
304
333
  }).then(checkStatus),
305
334
  },
335
+ batchOrders: {
336
+ create: async (payload) =>
337
+ fetchWithJWT(ROUTES.user.batchOrders.create, {
338
+ method: 'POST',
339
+ body: JSON.stringify(payload),
340
+ }).then(checkStatus),
341
+ get: async (filters?: Joanie.BatchOrderQueryFilters) => {
342
+ return fetchWithJWT(buildApiUrl(ROUTES.user.batchOrders.get, filters)).then(checkStatus);
343
+ },
344
+ submit_for_payment: {
345
+ create: async (filters) => {
346
+ return fetchWithJWT(
347
+ buildApiUrl(ROUTES.user.batchOrders.submit_for_payment.create, filters),
348
+ {
349
+ method: 'POST',
350
+ },
351
+ ).then(checkStatus);
352
+ },
353
+ },
354
+ },
306
355
  enrollments: {
307
356
  create: async (payload) =>
308
357
  fetchWithJWT(ROUTES.user.enrollments.create, {
@@ -406,6 +455,94 @@ const API = (): Joanie.API => {
406
455
  ).then(checkStatus);
407
456
  },
408
457
  },
458
+ quotes: {
459
+ get: async (filters?: Joanie.OrganizationQuoteQueryFilters) => {
460
+ return fetchWithJWT(buildApiUrl(ROUTES.organizations.quotes.get, filters), {
461
+ method: 'GET',
462
+ }).then(checkStatus);
463
+ },
464
+ update: async (filters: { organization_id: string; payload: any }) => {
465
+ if (filters.organization_id && filters.payload) {
466
+ return fetchWithJWT(
467
+ ROUTES.organizations.quotes.update.replace(
468
+ ':organization_id',
469
+ filters.organization_id,
470
+ ),
471
+ {
472
+ method: 'PATCH',
473
+ body: JSON.stringify(filters.payload),
474
+ },
475
+ ).then(checkStatus);
476
+ }
477
+ },
478
+ purchase_order: {
479
+ update: async (filters: { organization_id: string; payload: any }) => {
480
+ if (filters.organization_id && filters.payload) {
481
+ return fetchWithJWT(
482
+ ROUTES.organizations.quotes.purchase_order.update.replace(
483
+ ':organization_id',
484
+ filters.organization_id,
485
+ ),
486
+ {
487
+ method: 'PATCH',
488
+ body: JSON.stringify(filters.payload),
489
+ },
490
+ ).then(checkStatus);
491
+ }
492
+ },
493
+ },
494
+ bank_transfer: {
495
+ create: async (filters: { organization_id: string; payload: any }) => {
496
+ if (filters.organization_id && filters.payload) {
497
+ return fetchWithJWT(
498
+ ROUTES.organizations.quotes.bank_transfer.create.replace(
499
+ ':organization_id',
500
+ filters.organization_id,
501
+ ),
502
+ {
503
+ method: 'POST',
504
+ body: JSON.stringify(filters.payload),
505
+ },
506
+ ).then(checkStatus);
507
+ }
508
+ },
509
+ },
510
+ submit_for_signature: {
511
+ create: async (filters: { organization_id: string; payload: any }) => {
512
+ if (filters.organization_id && filters.payload) {
513
+ return fetchWithJWT(
514
+ ROUTES.organizations.quotes.submit_for_signature.create.replace(
515
+ ':organization_id',
516
+ filters.organization_id,
517
+ ),
518
+ {
519
+ method: 'POST',
520
+ body: JSON.stringify(filters.payload),
521
+ },
522
+ ).then(checkStatus);
523
+ }
524
+ },
525
+ },
526
+ download_quote: {
527
+ get: async (filters?: Joanie.OrganizationQuoteQueryFilters) => {
528
+ return fetchWithJWT(
529
+ buildApiUrl(ROUTES.organizations.quotes.download_quote.get, filters),
530
+ {
531
+ method: 'GET',
532
+ },
533
+ )
534
+ .then(checkStatus)
535
+ .then(getFileFromResponse);
536
+ },
537
+ },
538
+ },
539
+ agreements: {
540
+ get: async (filters) => {
541
+ return fetchWithJWT(buildApiUrl(ROUTES.organizations.agreements.get, filters), {
542
+ method: 'GET',
543
+ }).then(checkStatus);
544
+ },
545
+ },
409
546
  },
410
547
  courses: {
411
548
  get: (filters?: Joanie.CourseQueryFilters) => {
@@ -477,6 +614,13 @@ const API = (): Joanie.API => {
477
614
  : buildApiUrl(ROUTES.offerings.get, filters),
478
615
  ).then(checkStatus);
479
616
  },
617
+ organizations: {
618
+ get: (filters?: Joanie.OrganizationQuoteQueryFilters) => {
619
+ return fetchWithJWT(buildApiUrl(ROUTES.offerings.organizations.get, filters)).then(
620
+ checkStatus,
621
+ );
622
+ },
623
+ },
480
624
  },
481
625
  contractDefinitions: {
482
626
  previewTemplate(id: string): Promise<File> {
@@ -9,6 +9,7 @@ export enum SubscriptionErrorMessageId {
9
9
  ERROR_DEFAULT = 'errorDefault',
10
10
  ERROR_FULL_PRODUCT = 'errorFullProduct',
11
11
  ERROR_WITHDRAWAL_RIGHT = 'errorWithdrawalRight',
12
+ ERROR_BATCH_ORDER_FORM_INVALID = 'batchOrderFormInvalid',
12
13
  }
13
14
 
14
15
  export enum PaymentProviders {
@@ -17,6 +18,12 @@ export enum PaymentProviders {
17
18
  LYRA = 'lyra',
18
19
  }
19
20
 
21
+ export enum PaymentMethod {
22
+ CARD_PAYMENT = 'card_payment',
23
+ BANK_TRANSFER = 'bank_transfer',
24
+ PURCHASE_ORDER = 'purchase_order',
25
+ }
26
+
20
27
  export interface PaymentWithId {
21
28
  payment_id: string;
22
29
  }
@@ -59,8 +59,8 @@ export const PaymentScheduleGrid = ({ schedule }: Props) => {
59
59
  <DataGrid
60
60
  displayHeader={false}
61
61
  columns={[
62
- { field: 'index', size: 10 },
63
- { field: 'amount', size: 90 },
62
+ { field: 'index', size: 10, enableSorting: false },
63
+ { field: 'amount', size: 90, enableSorting: false },
64
64
  {
65
65
  field: 'date',
66
66
  renderCell: ({ row }) => (
@@ -68,6 +68,7 @@ export const PaymentScheduleGrid = ({ schedule }: Props) => {
68
68
  <FormattedMessage {...messages.withdrawnAt} values={{ date: row.date }} />
69
69
  </span>
70
70
  ),
71
+ enableSorting: false,
71
72
  },
72
73
  {
73
74
  id: 'state',
@@ -79,6 +80,7 @@ export const PaymentScheduleGrid = ({ schedule }: Props) => {
79
80
  ) : (
80
81
  ''
81
82
  ),
83
+ enableSorting: false,
82
84
  },
83
85
  ]}
84
86
  rows={schedule.map((installment, index) => ({
@@ -10,7 +10,7 @@ import {
10
10
  SaleTunnelContext,
11
11
  SaleTunnelContextType,
12
12
  } from 'components/SaleTunnel/GenericSaleTunnel';
13
- import { Address } from 'types/Joanie';
13
+ import { Address, PaymentSchedule } from 'types/Joanie';
14
14
  import {
15
15
  AddressFactory,
16
16
  CredentialOrderFactory,
@@ -50,6 +50,7 @@ describe('AddressSelector', () => {
50
50
  const Wrapper = () => {
51
51
  const [billingAddress, setBillingAddress] = useState<Address>();
52
52
  const [voucherCode, setVoucherCode] = useState<string>();
53
+ const [schedule, setSchedule] = useState<PaymentSchedule>();
53
54
  const context: SaleTunnelContextType = useMemo(
54
55
  () => ({
55
56
  webAnalyticsEventKey: 'eventKey',
@@ -58,6 +59,9 @@ describe('AddressSelector', () => {
58
59
  props: {} as SaleTunnelProps,
59
60
  billingAddress,
60
61
  setBillingAddress,
62
+ setBatchOrder: jest.fn(),
63
+ setBatchOrderFormMethods: jest.fn(),
64
+ validateBatchOrder: jest.fn(),
61
65
  setCreditCard: jest.fn(),
62
66
  step: SaleTunnelStep.IDLE,
63
67
  registerSubmitCallback: jest.fn(),
@@ -67,8 +71,11 @@ describe('AddressSelector', () => {
67
71
  hasWaivedWithdrawalRight: false,
68
72
  setHasWaivedWithdrawalRight: jest.fn(),
69
73
  setVoucherCode,
74
+ setSchedule,
75
+ needsPayment: false,
76
+ setNeedsPayment: jest.fn(),
70
77
  }),
71
- [billingAddress, voucherCode],
78
+ [billingAddress, voucherCode, schedule],
72
79
  );
73
80
  contextRef.current = context;
74
81
 
@@ -8,6 +8,7 @@ import {
8
8
  useState,
9
9
  useCallback,
10
10
  } from 'react';
11
+ import { UseFormReturn } from 'react-hook-form';
11
12
  import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
12
13
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
13
14
  import {
@@ -18,6 +19,8 @@ import {
18
19
  Order,
19
20
  OrderState,
20
21
  Product,
22
+ BatchOrder,
23
+ PaymentSchedule,
21
24
  } from 'types/Joanie';
22
25
  import useProductOrder from 'hooks/useProductOrder';
23
26
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
@@ -43,6 +46,11 @@ export interface SaleTunnelContextType {
43
46
  // meta
44
47
  billingAddress?: Address;
45
48
  setBillingAddress: (address?: Address) => void;
49
+ batchOrder?: BatchOrder;
50
+ setBatchOrder: (batchOrder?: BatchOrder) => void;
51
+ batchOrderFormMethods?: UseFormReturn<BatchOrder>;
52
+ setBatchOrderFormMethods: (methods?: UseFormReturn<BatchOrder>) => void;
53
+ validateBatchOrder: () => void;
46
54
  creditCard?: CreditCard;
47
55
  setCreditCard: (creditCard?: CreditCard) => void;
48
56
  hasWaivedWithdrawalRight: boolean;
@@ -53,6 +61,10 @@ export interface SaleTunnelContextType {
53
61
  nextStep: () => void;
54
62
  voucherCode?: string;
55
63
  setVoucherCode: (code?: string) => void;
64
+ schedule?: PaymentSchedule;
65
+ setSchedule: (schedule?: PaymentSchedule) => void;
66
+ needsPayment: boolean;
67
+ setNeedsPayment: (needsPayment: boolean) => void;
56
68
  }
57
69
 
58
70
  export const SaleTunnelContext = createContext<SaleTunnelContextType>({} as any);
@@ -89,6 +101,8 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
89
101
  productId: props.product.id,
90
102
  });
91
103
  const [billingAddress, setBillingAddress] = useState<Address>();
104
+ const [batchOrder, setBatchOrder] = useState<BatchOrder>();
105
+ const [batchOrderFormMethods, setBatchOrderFormMethods] = useState<UseFormReturn<BatchOrder>>();
92
106
  const [creditCard, setCreditCard] = useState<CreditCard>();
93
107
  const [hasWaivedWithdrawalRight, setHasWaivedWithdrawalRight] = useState(false);
94
108
  const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
@@ -96,6 +110,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
96
110
  new Map(),
97
111
  );
98
112
  const [voucherCode, setVoucherCode] = useState<string>();
113
+ const [needsPayment, setNeedsPayment] = useState(true);
99
114
 
100
115
  const nextStep = useCallback(() => {
101
116
  if (order)
@@ -123,6 +138,12 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
123
138
  }
124
139
  }, [order, step]);
125
140
 
141
+ const validateBatchOrder = useCallback(() => {
142
+ if (batchOrder && step === SaleTunnelStep.IDLE) setStep(SaleTunnelStep.SUCCESS);
143
+ }, [batchOrder]);
144
+
145
+ const [schedule, setSchedule] = useState<PaymentSchedule | undefined>(undefined);
146
+
126
147
  const context: SaleTunnelContextType = useMemo(
127
148
  () => ({
128
149
  webAnalyticsEventKey: props.eventKey,
@@ -133,6 +154,11 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
133
154
  props,
134
155
  billingAddress,
135
156
  setBillingAddress,
157
+ batchOrder,
158
+ setBatchOrder,
159
+ batchOrderFormMethods,
160
+ setBatchOrderFormMethods,
161
+ validateBatchOrder,
136
162
  creditCard,
137
163
  setCreditCard,
138
164
  hasWaivedWithdrawalRight,
@@ -154,16 +180,23 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
154
180
  },
155
181
  voucherCode,
156
182
  setVoucherCode,
183
+ schedule,
184
+ setSchedule,
185
+ needsPayment,
186
+ setNeedsPayment,
157
187
  }),
158
188
  [
159
189
  props,
160
190
  order,
161
191
  billingAddress,
192
+ batchOrder,
193
+ batchOrderFormMethods,
162
194
  creditCard,
163
195
  step,
164
196
  submitCallbacks,
165
197
  hasWaivedWithdrawalRight,
166
198
  voucherCode,
199
+ needsPayment,
167
200
  ],
168
201
  );
169
202
 
@@ -0,0 +1,253 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
+ import { FormProvider, useForm } from 'react-hook-form';
4
+ import { yupResolver } from '@hookform/resolvers/yup';
5
+ import * as Yup from 'yup';
6
+ import { Step, StepLabel, Stepper } from '@mui/material';
7
+ import { Button } from '@openfun/cunningham-react';
8
+ import { BatchOrder } from 'types/Joanie';
9
+ import Form from 'components/Form';
10
+ import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
11
+ import { ObjectHelper } from 'utils/ObjectHelper';
12
+ import { PaymentMethod } from 'components/PaymentInterfaces/types';
13
+ import { StepContent } from 'components/SaleTunnel/SaleTunnelInformation/StepContent';
14
+
15
+ const messages = defineMessages({
16
+ title: {
17
+ id: 'components.SaleTunnel.Information.title',
18
+ description: 'Title for the section containing purchase/billing information',
19
+ defaultMessage: 'Information',
20
+ },
21
+ description: {
22
+ id: 'components.SaleTunnel.Information.description',
23
+ description: 'Helper text explaining that the information will be used for billing',
24
+ defaultMessage: 'Those information will be used for billing',
25
+ },
26
+ purchaseTypeTitle: {
27
+ id: 'components.SaleTunnel.Information.purchaseTypeTitle',
28
+ description: 'Title of the section where the user selects the purchase type',
29
+ defaultMessage: 'Select purchase type',
30
+ },
31
+ purchaseTypeSelect: {
32
+ id: 'components.SaleTunnel.Information.purchaseTypeSelect',
33
+ description: 'Label for the select field used to choose the purchase type',
34
+ defaultMessage: 'Purchase type',
35
+ },
36
+ purchaseTypeOptionSingle: {
37
+ id: 'components.SaleTunnel.Information.purchaseTypeOptionSingle',
38
+ description: 'Option label for selecting a single purchase (B2C)',
39
+ defaultMessage: 'Single purchase (B2C)',
40
+ },
41
+ purchaseTypeOptionGroup: {
42
+ id: 'components.SaleTunnel.Information.purchaseTypeOptionGroup',
43
+ description: 'Option label for selecting a group purchase (B2B)',
44
+ defaultMessage: 'Group purchase (B2B)',
45
+ },
46
+ stepCompany: {
47
+ id: 'components.SaleTunnel.BatchOrderForm.stepCompany',
48
+ description: 'Step label for company information in the batch order form',
49
+ defaultMessage: 'Organization',
50
+ },
51
+ stepAdmin: {
52
+ id: 'components.SaleTunnel.BatchOrderForm.stepAdmin',
53
+ description: 'Step label for administrative follow-up in the batch order form',
54
+ defaultMessage: 'Follow-up',
55
+ },
56
+ stepSignatory: {
57
+ id: 'components.SaleTunnel.BatchOrderForm.stepSignatory',
58
+ description: 'Step label for signatory person in the batch order form',
59
+ defaultMessage: 'Signatory',
60
+ },
61
+ stepParticipants: {
62
+ id: 'components.SaleTunnel.BatchOrderForm.stepParticipants',
63
+ description: 'Step label for participants information in the batch order form',
64
+ defaultMessage: 'Participants',
65
+ },
66
+ stepFinancing: {
67
+ id: 'components.SaleTunnel.BatchOrderForm.stepFinancing',
68
+ description: 'Step label for financing/payment in the batch order form',
69
+ defaultMessage: 'Financing',
70
+ },
71
+ });
72
+
73
+ export const SaleTunnelInformationGroup = () => {
74
+ return (
75
+ <>
76
+ <div>
77
+ <h3 className="block-title mb-t">
78
+ <FormattedMessage {...messages.title} />
79
+ </h3>
80
+ <div className="description mb-s">
81
+ <FormattedMessage {...messages.description} />
82
+ </div>
83
+ </div>
84
+ <BatchOrderForm />
85
+ </>
86
+ );
87
+ };
88
+
89
+ export const validationSchema = Yup.object().shape({
90
+ offering_id: Yup.string().required(),
91
+ company_name: Yup.string().required(),
92
+ identification_number: Yup.string().required(),
93
+ vat_registration: Yup.string().optional(),
94
+ address: Yup.string().required(),
95
+ postcode: Yup.string().required(),
96
+ city: Yup.string().required(),
97
+ country: Yup.string().required(),
98
+ administrative_lastname: Yup.string().required(),
99
+ administrative_firstname: Yup.string().required(),
100
+ administrative_profession: Yup.string().required(),
101
+ administrative_email: Yup.string().email().required(),
102
+ administrative_telephone: Yup.string().required(),
103
+ signatory_lastname: Yup.string().required(),
104
+ signatory_firstname: Yup.string().required(),
105
+ signatory_profession: Yup.string().required(),
106
+ signatory_email: Yup.string().email().required(),
107
+ signatory_telephone: Yup.string().required(),
108
+ billing_address: Yup.object().optional().shape({
109
+ company_name: Yup.string().optional(),
110
+ identification_number: Yup.string().optional(),
111
+ contact_name: Yup.string().optional(),
112
+ contact_email: Yup.string().email().optional(),
113
+ address: Yup.string().optional(),
114
+ postcode: Yup.string().optional(),
115
+ city: Yup.string().optional(),
116
+ country: Yup.string().optional(),
117
+ }),
118
+ nb_seats: Yup.number().required().min(1),
119
+ payment_method: Yup.mixed<PaymentMethod>().oneOf(Object.values(PaymentMethod)).required(),
120
+ funding_entity: Yup.string().optional(),
121
+ funding_amount: Yup.number().optional(),
122
+ organization_id: Yup.string().optional(),
123
+ });
124
+
125
+ const requiredFieldsByStep: (keyof BatchOrder)[][] = [
126
+ ['company_name', 'identification_number', 'address', 'postcode', 'city', 'country'],
127
+ [
128
+ 'administrative_lastname',
129
+ 'administrative_firstname',
130
+ 'administrative_profession',
131
+ 'administrative_email',
132
+ 'administrative_telephone',
133
+ ],
134
+ [
135
+ 'signatory_lastname',
136
+ 'signatory_firstname',
137
+ 'signatory_profession',
138
+ 'signatory_email',
139
+ 'signatory_telephone',
140
+ ],
141
+ ['nb_seats'],
142
+ ['payment_method'],
143
+ ];
144
+
145
+ const BatchOrderForm = () => {
146
+ const intl = useIntl();
147
+ const { offering, batchOrder, setBatchOrder, setBatchOrderFormMethods } = useSaleTunnelContext();
148
+ const [isCurrentStepValid, setIsCurrentStepValid] = useState(false);
149
+ const defaultValues: BatchOrder = {
150
+ offering_id: offering?.id ?? '',
151
+ company_name: '',
152
+ identification_number: '',
153
+ address: '',
154
+ postcode: '',
155
+ city: '',
156
+ country: '',
157
+ administrative_lastname: '',
158
+ administrative_firstname: '',
159
+ administrative_profession: '',
160
+ administrative_email: '',
161
+ administrative_telephone: '',
162
+ signatory_lastname: '',
163
+ signatory_firstname: '',
164
+ signatory_profession: '',
165
+ signatory_email: '',
166
+ signatory_telephone: '',
167
+ nb_seats: 0,
168
+ payment_method: PaymentMethod.PURCHASE_ORDER,
169
+ funding_amount: 0,
170
+ };
171
+
172
+ const [activeStep, setActiveStep] = useState(0);
173
+ const steps = [
174
+ intl.formatMessage(messages.stepCompany),
175
+ intl.formatMessage(messages.stepAdmin),
176
+ intl.formatMessage(messages.stepSignatory),
177
+ intl.formatMessage(messages.stepParticipants),
178
+ intl.formatMessage(messages.stepFinancing),
179
+ ];
180
+
181
+ const form = useForm<BatchOrder>({
182
+ defaultValues: batchOrder || defaultValues,
183
+ mode: 'onBlur',
184
+ resolver: yupResolver(validationSchema),
185
+ });
186
+ const { watch } = form;
187
+ const values = watch();
188
+
189
+ useEffect(() => {
190
+ setBatchOrderFormMethods(form);
191
+ }, [form]);
192
+
193
+ useEffect(() => {
194
+ const cleanedValues = ObjectHelper.removeEmptyFields(values);
195
+ if (JSON.stringify(cleanedValues) !== JSON.stringify(batchOrder)) {
196
+ setBatchOrder(cleanedValues);
197
+ }
198
+ }, [values, batchOrder, setBatchOrder]);
199
+
200
+ useEffect(() => {
201
+ const requiredRules = requiredFieldsByStep[activeStep];
202
+ const isStepValid = requiredRules.every((field) => !!batchOrder?.[field]);
203
+ setIsCurrentStepValid(isStepValid);
204
+ }, [activeStep, batchOrder]);
205
+
206
+ return (
207
+ <FormProvider {...form}>
208
+ <Form noValidate>
209
+ <Stepper activeStep={activeStep} alternativeLabel className="stepper">
210
+ {steps.map((label, index) => (
211
+ <Step key={label}>
212
+ <StepLabel
213
+ onClick={() => {
214
+ if (index < activeStep) {
215
+ setActiveStep(index);
216
+ }
217
+ }}
218
+ style={{ cursor: index < activeStep ? 'pointer' : 'default' }}
219
+ >
220
+ {label}
221
+ </StepLabel>
222
+ </Step>
223
+ ))}
224
+ </Stepper>
225
+ <StepContent activeStep={activeStep} form={form} />
226
+ </Form>
227
+ <div className="navigationButton">
228
+ <Button
229
+ onClick={() => {
230
+ if (activeStep > 0) {
231
+ setActiveStep(activeStep - 1);
232
+ }
233
+ }}
234
+ hidden={activeStep === 0}
235
+ color="secondary"
236
+ >
237
+ Previous
238
+ </Button>
239
+ <Button
240
+ onClick={() => {
241
+ if (activeStep < steps.length - 1) {
242
+ setActiveStep(activeStep + 1);
243
+ }
244
+ }}
245
+ disabled={!isCurrentStepValid}
246
+ hidden={activeStep === steps.length - 1}
247
+ >
248
+ Next
249
+ </Button>
250
+ </div>
251
+ </FormProvider>
252
+ );
253
+ };