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
@@ -1,282 +1,68 @@
1
- import { ChangeEvent, useEffect, useState } from 'react';
2
- import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
3
- import { Alert, Button, Input, VariantType } from '@openfun/cunningham-react';
4
- import { AddressSelector } from 'components/SaleTunnel/AddressSelector';
5
- import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
1
+ import { useState } from 'react';
2
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
+ import { Select } from '@openfun/cunningham-react';
6
4
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
7
- import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm';
8
- import { useSession } from 'contexts/SessionContext';
9
- import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
10
- import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
11
- import { PaymentSchedule, ProductType } from 'types/Joanie';
12
- import { usePaymentPlan } from 'hooks/usePaymentPlan';
5
+ import { SaleTunnelInformationSingular } from 'components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular';
6
+ import { SaleTunnelInformationGroup } from 'components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup';
13
7
 
14
8
  const messages = defineMessages({
15
- title: {
16
- id: 'components.SaleTunnel.Information.title',
17
- description: 'Title for the information section',
18
- defaultMessage: 'Information',
9
+ purchaseTypeTitle: {
10
+ id: 'components.SaleTunnel.Information.purchaseTypeTitle',
11
+ description: 'Title for purchase type',
12
+ defaultMessage: 'Select purchase type',
19
13
  },
20
- description: {
21
- id: 'components.SaleTunnel.Information.description',
22
- description: 'Description of the information section',
23
- defaultMessage: 'Those information will be used for billing',
14
+ purchaseTypeSelect: {
15
+ id: 'components.SaleTunnel.Information.purchaseTypeSelect',
16
+ description: 'Label for purchase type select',
17
+ defaultMessage: 'Purchase type',
24
18
  },
25
- fullNameLabel: {
26
- id: 'components.SaleTunnel.Information.fullNameLabel',
27
- description: 'Label for the full name input',
28
- defaultMessage: 'Full name',
19
+ purchaseTypeOptionSingle: {
20
+ id: 'components.SaleTunnel.Information.purchaseTypeOptionSingle',
21
+ description: 'Label for B2C option',
22
+ defaultMessage: 'Single purchase (B2C)',
29
23
  },
30
- paymentSchedule: {
31
- id: 'components.SaleTunnel.Information.paymentSchedule',
32
- description: 'Title for the payment schedule section',
33
- defaultMessage: 'Payment schedule',
34
- },
35
- totalInfo: {
36
- id: 'components.SaleTunnel.Information.total.info',
37
- description: 'Information about the total amount',
38
- defaultMessage: 'You will then pay on the secured platform of our payment provider.',
39
- },
40
- totalLabel: {
41
- id: 'components.SaleTunnel.Information.total.label',
42
- description: 'Label for the total amount',
43
- defaultMessage: 'Total',
44
- },
45
- emailLabel: {
46
- id: 'components.SaleTunnel.Information.email.label',
47
- description: 'Label for the email',
48
- defaultMessage: 'Account email',
49
- },
50
- emailInfo: {
51
- id: 'components.SaleTunnel.Information.email.info',
52
- description: 'Info for the email',
53
- defaultMessage:
54
- 'This email will be used to send you confirmation mails, it is the one you created your account with.',
55
- },
56
- voucherTitle: {
57
- id: 'components.SaleTunnel.Information.voucher.title',
58
- description: 'Title for the voucher',
59
- defaultMessage: 'Voucher code',
60
- },
61
- voucherInfo: {
62
- id: 'components.SaleTunnel.Information.voucher.info',
63
- description: 'Info for the voucher',
64
- defaultMessage: 'If you have a voucher code, please enter it in the field below.',
65
- },
66
- voucherValidate: {
67
- id: 'components.SaleTunnel.Information.voucher.validate',
68
- description: 'Validate text for the voucher',
69
- defaultMessage: 'Validate',
70
- },
71
- voucherDelete: {
72
- id: 'components.SaleTunnel.Information.voucher.delete',
73
- description: 'Delete text for the voucher',
74
- defaultMessage: 'Delete this voucher',
75
- },
76
- voucherError: {
77
- id: 'components.SaleTunnel.Information.voucher.error',
78
- description: 'Error when voucher is invalid',
79
- defaultMessage: 'The submitted voucher code is not valid.',
80
- },
81
- discount: {
82
- id: 'components.SaleTunnel.Information.voucher.discount',
83
- description: 'Discount description',
84
- defaultMessage: 'Discount applied',
24
+ purchaseTypeOptionGroup: {
25
+ id: 'components.SaleTunnel.Information.purchaseTypeOptionGroup',
26
+ description: 'Label for B2C option',
27
+ defaultMessage: 'Group purchase (B2B)',
85
28
  },
86
29
  });
87
30
 
88
- export const SaleTunnelInformation = () => {
89
- const { props, product, voucherCode, setVoucherCode } = useSaleTunnelContext();
90
- const [voucherError, setVoucherError] = useState(false);
91
- const query = usePaymentPlan({
92
- course_code: props.course?.code ?? props.enrollment!.course_run.course.code,
93
- product_id: props.product.id,
94
- ...(voucherCode ? { voucher_code: voucherCode } : {}),
95
- });
96
- const schedule = query.data?.payment_schedule ?? props.paymentPlan?.payment_schedule;
97
- const price = query.data?.price ?? props.paymentPlan?.price;
98
- const discountedPrice = query.data?.discounted_price ?? props.paymentPlan?.discounted_price;
99
- const discount = query.data?.discount ?? props.paymentPlan?.discount;
100
-
101
- const showPaymentSchedule =
102
- product.type === ProductType.CREDENTIAL &&
103
- schedule &&
104
- (discountedPrice != null ? discountedPrice > 0 : price != null && price > 0);
31
+ export enum FormType {
32
+ GROUP = 'group',
33
+ SINGULAR = 'singular',
34
+ }
105
35
 
106
- useEffect(() => {
107
- if (query.error && voucherCode) {
108
- setVoucherCode('');
109
- setVoucherError(true);
110
- }
111
- }, [query.error, voucherCode, setVoucherCode]);
36
+ export const SaleTunnelInformation = () => {
37
+ const intl = useIntl();
38
+ const { setBatchOrder, setSchedule } = useSaleTunnelContext();
39
+ const options = [
40
+ { label: intl.formatMessage(messages.purchaseTypeOptionSingle), value: FormType.SINGULAR },
41
+ { label: intl.formatMessage(messages.purchaseTypeOptionGroup), value: FormType.GROUP },
42
+ ];
43
+ const [purchaseType, setPurchaseType] = useState(FormType.SINGULAR);
112
44
 
113
45
  return (
114
46
  <div className="sale-tunnel__main__column sale-tunnel__information">
115
47
  <div>
116
48
  <h3 className="block-title mb-t">
117
- <FormattedMessage {...messages.title} />
49
+ <FormattedMessage {...messages.purchaseTypeTitle} />
118
50
  </h3>
119
- <div className="description mb-s">
120
- <FormattedMessage {...messages.description} />
121
- </div>
122
- <OpenEdxFullNameForm />
123
- <AddressSelector />
124
- <div className="mt-s">
125
- <Email />
126
- </div>
127
- </div>
128
- <div>
129
- {showPaymentSchedule && <PaymentScheduleBlock schedule={schedule} />}
130
- <Voucher
131
- discount={discount}
132
- voucherError={voucherError}
133
- setVoucherError={setVoucherError}
51
+ <Select
52
+ label={intl.formatMessage(messages.purchaseTypeSelect)}
53
+ options={options}
54
+ fullWidth
55
+ value={purchaseType}
56
+ clearable={false}
57
+ onChange={(e) => {
58
+ setPurchaseType(e.target.value as FormType);
59
+ setBatchOrder(undefined);
60
+ setSchedule(undefined);
61
+ }}
134
62
  />
135
- <Total price={price} discountedPrice={discountedPrice} />
136
- <WithdrawRightCheckbox />
137
- </div>
138
- </div>
139
- );
140
- };
141
-
142
- const Email = () => {
143
- const { user } = useSession();
144
- const { data: openEdxProfileData } = useOpenEdxProfile({
145
- username: user!.username,
146
- });
147
-
148
- return (
149
- <div className="sale-tunnel__email">
150
- <div className="sale-tunnel__email__top">
151
- <h4>
152
- <FormattedMessage {...messages.emailLabel} />
153
- </h4>
154
- <div className="fw-bold">{openEdxProfileData?.email}</div>
155
- </div>
156
- <div className="sale-tunnel__email__description">
157
- <FormattedMessage {...messages.emailInfo} />
158
- </div>
159
- </div>
160
- );
161
- };
162
-
163
- const Total = ({ price, discountedPrice }: { price?: number; discountedPrice?: number }) => {
164
- const { product } = useSaleTunnelContext();
165
- const totalPrice = price || product.price;
166
- return (
167
- <div className="sale-tunnel__total">
168
- <div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
169
- <div className="block-title">
170
- <FormattedMessage {...messages.totalLabel} />
171
- </div>
172
-
173
- <div className="block-title">
174
- {discountedPrice !== undefined ? (
175
- <>
176
- <span className="price--striked">
177
- <FormattedNumber
178
- value={totalPrice}
179
- style="currency"
180
- currency={product.price_currency}
181
- />
182
- </span>
183
- <FormattedNumber
184
- value={discountedPrice}
185
- style="currency"
186
- currency={product.price_currency}
187
- />
188
- </>
189
- ) : (
190
- <FormattedNumber
191
- value={totalPrice}
192
- style="currency"
193
- currency={product.price_currency}
194
- />
195
- )}
196
- </div>
197
- </div>
198
- </div>
199
- );
200
- };
201
-
202
- const Voucher = ({
203
- discount,
204
- voucherError,
205
- setVoucherError,
206
- }: {
207
- discount?: string;
208
- voucherError: boolean;
209
- setVoucherError: (value: boolean) => void;
210
- }) => {
211
- const intl = useIntl();
212
- const { voucherCode, setVoucherCode } = useSaleTunnelContext();
213
- const [voucher, setVoucher] = useState('');
214
- const handleVoucher = (e: ChangeEvent<HTMLInputElement>) => setVoucher(e.target.value);
215
- const submitVoucher = () => {
216
- setVoucherError(false);
217
- setVoucherCode(voucher);
218
- setVoucher('');
219
- };
220
-
221
- return (
222
- <div className="sale-tunnel__voucher">
223
- <div className="description">
224
- <h4 className="block-title mb-t">
225
- <FormattedMessage {...messages.voucherTitle} />
226
- </h4>
227
- <span className="mb-t">
228
- <FormattedMessage {...messages.voucherInfo} />
229
- </span>
230
- </div>
231
- <div className="form">
232
- <Input
233
- className="form-field mt-s"
234
- value={voucher}
235
- onChange={handleVoucher}
236
- label={intl.formatMessage(messages.voucherTitle)}
237
- disabled={!!voucherCode}
238
- />
239
- <Button size="small" color="primary" onClick={submitVoucher} disabled={!!voucherCode}>
240
- <FormattedMessage {...messages.voucherValidate} />
241
- </Button>
242
- </div>
243
- {voucherCode && (
244
- <div className="voucher-tag">
245
- <span>{voucherCode}</span>
246
- <button
247
- onClick={() => setVoucherCode('')}
248
- title={intl.formatMessage(messages.voucherDelete)}
249
- >
250
- <span className="material-icons">close</span>
251
- </button>
252
- </div>
253
- )}
254
- {discount && (
255
- <div className="voucher-discount">
256
- <span>
257
- <FormattedMessage {...messages.discount} />
258
- </span>
259
- <span>{discount}</span>
260
- </div>
261
- )}
262
- {voucherError && (
263
- <Alert type={VariantType.ERROR} className="mt-s">
264
- <FormattedMessage {...messages.voucherError} />
265
- </Alert>
266
- )}
267
- </div>
268
- );
269
- };
270
-
271
- const PaymentScheduleBlock = ({ schedule }: { schedule: PaymentSchedule }) => {
272
- return (
273
- <div className="payment-schedule">
274
- <h4 className="block-title mb-t">
275
- <FormattedMessage {...messages.paymentSchedule} />
276
- </h4>
277
- <div className="mt-t">
278
- <PaymentScheduleGrid schedule={schedule} />
279
63
  </div>
64
+ {purchaseType === FormType.SINGULAR && <SaleTunnelInformationSingular />}
65
+ {purchaseType === FormType.GROUP && <SaleTunnelInformationGroup />}
280
66
  </div>
281
67
  );
282
68
  };
@@ -33,7 +33,7 @@ const messages = defineMessages({
33
33
 
34
34
  export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) => {
35
35
  const intl = useIntl();
36
- const { order, product } = useSaleTunnelContext();
36
+ const { order, product, batchOrder, schedule } = useSaleTunnelContext();
37
37
 
38
38
  return (
39
39
  <section className="sale-tunnel-step" data-testid="generic-sale-tunnel-success-step">
@@ -46,24 +46,38 @@ export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) =>
46
46
  <p className="sale-tunnel-step__content">
47
47
  <FormattedMessage {...messages.successMessage} />
48
48
  <br />
49
- <FormattedMessage {...messages.successDetailMessage} />
49
+ {schedule && <FormattedMessage {...messages.successDetailMessage} />}
50
50
  </p>
51
- <footer className="sale-tunnel-step__footer">
52
- {product.type === ProductType.CREDENTIAL ? (
51
+ {order && (
52
+ <footer className="sale-tunnel-step__footer">
53
+ {product.type === ProductType.CREDENTIAL ? (
54
+ <Button
55
+ href={
56
+ getDashboardBasename(intl.locale) +
57
+ generatePath(LearnerDashboardPaths.ORDER, { orderId: order!.id })
58
+ }
59
+ >
60
+ <FormattedMessage {...messages.cta} />
61
+ </Button>
62
+ ) : (
63
+ <Button onClick={closeModal}>
64
+ <FormattedMessage {...messages.cta} />
65
+ </Button>
66
+ )}
67
+ </footer>
68
+ )}
69
+ {batchOrder?.id && (
70
+ <footer className="sale-tunnel-step__footer">
53
71
  <Button
54
72
  href={
55
73
  getDashboardBasename(intl.locale) +
56
- generatePath(LearnerDashboardPaths.ORDER, { orderId: order!.id })
74
+ generatePath(LearnerDashboardPaths.BATCH_ORDER, { batchOrderId: batchOrder.id })
57
75
  }
58
76
  >
59
77
  <FormattedMessage {...messages.cta} />
60
78
  </Button>
61
- ) : (
62
- <Button onClick={closeModal}>
63
- <FormattedMessage {...messages.cta} />
64
- </Button>
65
- )}
66
- </footer>
79
+ </footer>
80
+ )}
67
81
  </section>
68
82
  );
69
83
  };
@@ -2,7 +2,9 @@ import { useEffect, useMemo, useState } from 'react';
2
2
  import { Alert, Button, VariantType } from '@openfun/cunningham-react';
3
3
  import { defineMessages, FormattedMessage } from 'react-intl';
4
4
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
5
+ import { validationSchema } from 'components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup';
5
6
  import { useOrders } from 'hooks/useOrders';
7
+ import { useBatchOrder } from 'hooks/useBatchOrder';
6
8
  import { OrderCreationPayload } from 'types/Joanie';
7
9
  import { useMatchMediaLg } from 'hooks/useMatchMedia';
8
10
  import { SubscriptionErrorMessageId } from 'components/PaymentInterfaces/types';
@@ -60,6 +62,11 @@ const messages = defineMessages({
60
62
  description: 'Label for screen reader when an order creation is in progress.',
61
63
  id: 'components.SubscriptionButton.orderCreationInProgress',
62
64
  },
65
+ batchOrderFormInvalid: {
66
+ id: 'components.SubscriptionButton.batchOrderFormInvalid',
67
+ defaultMessage: 'Some required fields are missing in the form.',
68
+ description: 'Some required fields are missing in the form.',
69
+ },
63
70
  });
64
71
 
65
72
  enum ComponentStates {
@@ -82,16 +89,23 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
82
89
  order,
83
90
  creditCard,
84
91
  billingAddress,
92
+ batchOrder,
93
+ setBatchOrder,
94
+ batchOrderFormMethods,
95
+ validateBatchOrder,
85
96
  hasWaivedWithdrawalRight,
86
97
  product,
87
98
  nextStep,
88
99
  runSubmitCallbacks,
89
100
  props: saleTunnelProps,
90
101
  voucherCode,
102
+ needsPayment,
91
103
  } = useSaleTunnelContext();
92
104
  const { methods: orderMethods } = useOrders(undefined, { enabled: false });
105
+ const { methods: batchOrderMethods } = useBatchOrder();
93
106
  const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
94
107
  const [error, setError] = useState<SubscriptionErrorMessageId | string>();
108
+ const [isBatchOrderValid, setIsBatchOrderValid] = useState(false);
95
109
  const isMobile = useMatchMediaLg();
96
110
 
97
111
  const handleError = (
@@ -112,12 +126,12 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
112
126
  return;
113
127
  }
114
128
 
115
- if (!billingAddress) {
129
+ if (!billingAddress && needsPayment) {
116
130
  handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
117
131
  return;
118
132
  }
119
133
 
120
- if (!saleTunnelProps.isWithdrawable && !hasWaivedWithdrawalRight) {
134
+ if (!saleTunnelProps.isWithdrawable && !hasWaivedWithdrawalRight && needsPayment) {
121
135
  handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
122
136
  return;
123
137
  }
@@ -142,15 +156,43 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
142
156
  });
143
157
  };
144
158
 
159
+ const createBatchOrder = async () => {
160
+ setState(ComponentStates.LOADING);
161
+ try {
162
+ await runSubmitCallbacks();
163
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
164
+ } catch (_error) {
165
+ setState(ComponentStates.IDLE);
166
+ return;
167
+ }
168
+ if (!batchOrder) return;
169
+ const isFormValid = await batchOrderFormMethods?.trigger();
170
+ if (!isFormValid) {
171
+ handleError(SubscriptionErrorMessageId.ERROR_BATCH_ORDER_FORM_INVALID);
172
+ return;
173
+ }
174
+ batchOrderMethods.create(batchOrder, {
175
+ onError: async () => {
176
+ handleError();
177
+ },
178
+ onSuccess: async (createdBatchOrder: any) => {
179
+ if (createdBatchOrder.id) {
180
+ setBatchOrder(createdBatchOrder);
181
+ validateBatchOrder();
182
+ }
183
+ },
184
+ });
185
+ };
186
+
145
187
  const walkthroughMessages = useMemo(() => {
146
188
  if (product.contract_definition && product.price > 0) {
147
189
  return messages.walkthroughToSignAndSavePayment;
148
190
  } else if (product.contract_definition && product.price === 0) {
149
191
  return messages.walkthroughToSign;
150
- } else if (!product.contract_definition && product.price > 0) {
192
+ } else if (!product.contract_definition && product.price > 0 && needsPayment) {
151
193
  return messages.walkthroughToSavePayment;
152
194
  }
153
- }, [product, creditCard]);
195
+ }, [product, creditCard, needsPayment]);
154
196
 
155
197
  useEffect(() => {
156
198
  if (order) nextStep();
@@ -165,6 +207,12 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
165
207
  }
166
208
  }, [state]);
167
209
 
210
+ useEffect(() => {
211
+ if (batchOrder) {
212
+ validationSchema.isValid(batchOrder).then((isValid) => setIsBatchOrderValid(isValid));
213
+ }
214
+ }, [batchOrder]);
215
+
168
216
  return (
169
217
  <>
170
218
  <div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
@@ -178,9 +226,9 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
178
226
  )}
179
227
  </div>
180
228
  <Button
181
- onClick={createOrder}
229
+ onClick={batchOrder ? createBatchOrder : createOrder}
182
230
  fullWidth={isMobile}
183
- disabled={state === ComponentStates.LOADING}
231
+ disabled={state === ComponentStates.LOADING || (batchOrder && !isBatchOrderValid)}
184
232
  {...(state === ComponentStates.ERROR && {
185
233
  'aria-describedby': 'sale-tunnel-payment-error',
186
234
  })}
@@ -130,6 +130,61 @@
130
130
  font-size: var(--c--theme--font--sizes--l);
131
131
  }
132
132
  }
133
+
134
+ .stepper {
135
+ flex-wrap: wrap;
136
+ gap: 0.2rem;
137
+ }
138
+
139
+ .navigationButton {
140
+ display: flex;
141
+ justify-content: center;
142
+ gap: 2rem;
143
+ }
144
+
145
+ .step-content {
146
+ .step {
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: center;
150
+ margin: 1rem 0;
151
+ gap: 0.5rem;
152
+ .field {
153
+ width: 90%;
154
+ }
155
+ .city-fields {
156
+ display: flex;
157
+ flex-direction: row;
158
+ justify-content: space-between;
159
+ gap: 0.25rem;
160
+ width: 90%;
161
+ }
162
+ .payment-block {
163
+ & > div {
164
+ margin: 1rem 0;
165
+ flex-direction: row;
166
+ }
167
+ }
168
+ .organism-block {
169
+ width: 100%;
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.25rem;
173
+ margin-bottom: 1rem;
174
+ .opco-order {
175
+ display: flex;
176
+ flex-direction: row;
177
+ gap: 0.25rem;
178
+ & > div {
179
+ width: 100%;
180
+ }
181
+ }
182
+ }
183
+ .recommandation {
184
+ width: 100%;
185
+ }
186
+ }
187
+ }
133
188
  }
134
189
 
135
190
  .description {