richie-education 3.1.3-dev3 → 3.1.3-dev30
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/.storybook/__mocks__/utils/context.ts +4 -0
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +18 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +4 -0
- package/js/components/CourseGlimpse/utils.ts +35 -30
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
- package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +171 -29
- package/js/components/SaleTunnel/index.stories.tsx +17 -3
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/types/Course.ts +4 -0
- package/js/types/Joanie.ts +36 -29
- package/js/types/index.ts +6 -2
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +19 -25
- package/js/utils/test/factories/richie.ts +10 -2
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +35 -8
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -8
- package/package.json +1 -1
- package/scss/objects/_course_glimpses.scss +16 -0
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Children, useEffect, useMemo } from 'react';
|
|
2
2
|
import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
|
|
3
3
|
import c from 'classnames';
|
|
4
|
-
import {
|
|
4
|
+
import { Offering, CredentialOrder, Product, ProductType } from 'types/Joanie';
|
|
5
5
|
import { useCourseProduct } from 'hooks/useCourseProducts';
|
|
6
6
|
import { Spinner } from 'components/Spinner';
|
|
7
7
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
@@ -38,6 +38,31 @@ const messages = defineMessages({
|
|
|
38
38
|
description: 'Course run languages',
|
|
39
39
|
id: 'components.CourseProductItem.availableIn',
|
|
40
40
|
},
|
|
41
|
+
original_price: {
|
|
42
|
+
defaultMessage: 'Original price:',
|
|
43
|
+
description: 'Label for the original price of a product',
|
|
44
|
+
id: 'components.CourseProductItem.original_price',
|
|
45
|
+
},
|
|
46
|
+
discounted_price: {
|
|
47
|
+
defaultMessage: 'Discounted price:',
|
|
48
|
+
description: 'Label for the discounted price of a product',
|
|
49
|
+
id: 'components.CourseProductItem.discounted_price',
|
|
50
|
+
},
|
|
51
|
+
discount_rate: {
|
|
52
|
+
defaultMessage: '-{rate}%',
|
|
53
|
+
description: 'Discount rate information',
|
|
54
|
+
id: 'components.CourseProductItem.discount_rate',
|
|
55
|
+
},
|
|
56
|
+
from: {
|
|
57
|
+
defaultMessage: 'from {from}',
|
|
58
|
+
description: 'Discount start date information',
|
|
59
|
+
id: 'components.CourseProductItem.from',
|
|
60
|
+
},
|
|
61
|
+
to: {
|
|
62
|
+
defaultMessage: 'to {to}',
|
|
63
|
+
description: 'Discount end date information',
|
|
64
|
+
id: 'components.CourseProductItem.to',
|
|
65
|
+
},
|
|
41
66
|
});
|
|
42
67
|
|
|
43
68
|
export interface CourseProductItemProps {
|
|
@@ -52,8 +77,9 @@ type HeaderProps = {
|
|
|
52
77
|
canPurchase: boolean;
|
|
53
78
|
order: Maybe<CredentialOrder>;
|
|
54
79
|
product: Product;
|
|
80
|
+
offering: Offering;
|
|
55
81
|
};
|
|
56
|
-
const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
82
|
+
const Header = ({ product, order, offering, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
57
83
|
const intl = useIntl();
|
|
58
84
|
const formatDate = useDateFormat();
|
|
59
85
|
|
|
@@ -72,21 +98,90 @@ const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderPr
|
|
|
72
98
|
return ProductHelper.getLanguages(product, true, intl);
|
|
73
99
|
}, [canShowMetadata, product, intl]);
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
const displayPrice = useMemo(() => {
|
|
102
|
+
if (!canPurchase) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (offering.rules.discounted_price != null) {
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<span id="original-price" className="offscreen">
|
|
110
|
+
<FormattedMessage {...messages.original_price} />
|
|
111
|
+
</span>
|
|
112
|
+
<del aria-describedby="original-price" className="product-widget__price-discounted">
|
|
82
113
|
<FormattedNumber
|
|
83
114
|
currency={product.price_currency}
|
|
84
115
|
value={product.price}
|
|
85
116
|
style="currency"
|
|
86
117
|
/>
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
</del>
|
|
119
|
+
<span id="discount-price" className="offscreen">
|
|
120
|
+
<FormattedMessage {...messages.discounted_price} />
|
|
121
|
+
</span>
|
|
122
|
+
<ins aria-describedby="discount-price" className="product-widget__price-discount">
|
|
123
|
+
<FormattedNumber
|
|
124
|
+
currency={product.price_currency}
|
|
125
|
+
value={offering.rules.discounted_price}
|
|
126
|
+
style="currency"
|
|
127
|
+
/>
|
|
128
|
+
</ins>
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
|
|
135
|
+
);
|
|
136
|
+
}, [canPurchase, offering.rules.discounted_price, product.price]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<header className="product-widget__header">
|
|
140
|
+
<div className="product-widget__header-main">
|
|
141
|
+
<h3 className="product-widget__title">{product.title}</h3>
|
|
89
142
|
</div>
|
|
143
|
+
<strong className="product-widget__price h6">
|
|
144
|
+
{hasPurchased && <FormattedMessage {...messages.purchased} />}
|
|
145
|
+
{displayPrice}
|
|
146
|
+
</strong>
|
|
147
|
+
{offering?.rules.description && (
|
|
148
|
+
<p className="product-widget__header-description">{offering.rules.description}</p>
|
|
149
|
+
)}
|
|
150
|
+
{offering?.rules.discounted_price && (
|
|
151
|
+
<p className="product-widget__header-discount">
|
|
152
|
+
{offering.rules.discount_rate ? (
|
|
153
|
+
<span className="product-widget__header-discount-rate">
|
|
154
|
+
<FormattedNumber value={-offering.rules.discount_rate} style="percent" />
|
|
155
|
+
</span>
|
|
156
|
+
) : (
|
|
157
|
+
<span className="product-widget__header-discount-amount">
|
|
158
|
+
<FormattedNumber
|
|
159
|
+
currency={product.price_currency}
|
|
160
|
+
value={-offering.rules.discount_amount!}
|
|
161
|
+
style="currency"
|
|
162
|
+
/>
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
{offering.rules.discount_start && (
|
|
166
|
+
<span className="product-widget__header-discount-date">
|
|
167
|
+
|
|
168
|
+
<FormattedMessage
|
|
169
|
+
{...messages.from}
|
|
170
|
+
values={{ from: formatDate(offering.rules.discount_start) }}
|
|
171
|
+
/>
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
{offering.rules.discount_end && (
|
|
175
|
+
<span className="product-widget__header-discount-date">
|
|
176
|
+
|
|
177
|
+
<FormattedMessage
|
|
178
|
+
{...messages.to}
|
|
179
|
+
values={{ to: formatDate(offering.rules.discount_end) }}
|
|
180
|
+
/>
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</p>
|
|
184
|
+
)}
|
|
90
185
|
{canShowMetadata && (
|
|
91
186
|
<>
|
|
92
187
|
<p
|
|
@@ -131,7 +226,7 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
131
226
|
<ol className="product-widget__content">
|
|
132
227
|
{Children.toArray(
|
|
133
228
|
targetCourses.map((target_course) => (
|
|
134
|
-
<CourseRunItem targetCourse={target_course} order={order} />
|
|
229
|
+
<CourseRunItem key={target_course.code} targetCourse={target_course} order={order} />
|
|
135
230
|
)),
|
|
136
231
|
)}
|
|
137
232
|
{product.certificate_definition && (
|
|
@@ -144,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
144
239
|
const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
|
|
145
240
|
// FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
|
|
146
241
|
// CredentialOrder
|
|
147
|
-
const { item:
|
|
242
|
+
const { item: offering, states: productQueryStates } = useCourseProduct({
|
|
148
243
|
product_id: productId,
|
|
149
244
|
course_id: course.code,
|
|
150
245
|
});
|
|
151
246
|
|
|
152
|
-
const product =
|
|
247
|
+
const product = offering?.product;
|
|
153
248
|
const { item: productOrder, states: orderQueryStates } = useProductOrder({
|
|
154
249
|
productId,
|
|
155
250
|
courseCode: course.code,
|
|
@@ -179,13 +274,6 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
179
274
|
return null;
|
|
180
275
|
}
|
|
181
276
|
|
|
182
|
-
const orderGroups = courseProductRelation
|
|
183
|
-
? ProductHelper.getActiveOrderGroups(courseProductRelation)
|
|
184
|
-
: [];
|
|
185
|
-
const orderGroupsAvailable = orderGroups.filter(
|
|
186
|
-
(orderGroup) => orderGroup.nb_available_seats > 0,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
277
|
return (
|
|
190
278
|
<section
|
|
191
279
|
className={c('product-widget', {
|
|
@@ -214,6 +302,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
214
302
|
<Header
|
|
215
303
|
product={product}
|
|
216
304
|
order={order}
|
|
305
|
+
offering={offering}
|
|
217
306
|
canPurchase={canPurchase}
|
|
218
307
|
hasPurchased={hasPurchased}
|
|
219
308
|
compact={compact}
|
|
@@ -222,9 +311,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
222
311
|
<footer className="product-widget__footer">
|
|
223
312
|
<CourseProductItemFooter
|
|
224
313
|
course={course}
|
|
225
|
-
|
|
226
|
-
orderGroups={orderGroups}
|
|
227
|
-
orderGroupsAvailable={orderGroupsAvailable}
|
|
314
|
+
offering={offering}
|
|
228
315
|
canPurchase={canPurchase}
|
|
229
316
|
/>
|
|
230
317
|
</footer>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { CourseRunFactory, PacedCourseFactory } from 'utils/test/factories/richie';
|
|
3
|
+
import { StorybookHelper } from 'utils/StorybookHelper';
|
|
4
|
+
import { CourseCertificateOffer, CourseOffer } from '../../../../types/Course';
|
|
5
|
+
import { SyllabusCourseRun } from '.';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
component: SyllabusCourseRun,
|
|
9
|
+
render: (args) => {
|
|
10
|
+
return StorybookHelper.wrapInApp(<SyllabusCourseRun {...args} />);
|
|
11
|
+
},
|
|
12
|
+
} as Meta<typeof SyllabusCourseRun>;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof SyllabusCourseRun>;
|
|
15
|
+
|
|
16
|
+
const courseRun = CourseRunFactory().one();
|
|
17
|
+
|
|
18
|
+
export const certificateSyllabusCourseRun: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
courseRun: {
|
|
21
|
+
...courseRun,
|
|
22
|
+
title: 'Certificate Product',
|
|
23
|
+
price_currency: 'EUR',
|
|
24
|
+
offer: CourseOffer.FREE,
|
|
25
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
26
|
+
certificate_price: 100,
|
|
27
|
+
discounted_price: null,
|
|
28
|
+
discount: null,
|
|
29
|
+
},
|
|
30
|
+
course: PacedCourseFactory().one(),
|
|
31
|
+
showLanguages: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export const certificateDiscountSyllabusCourseRun: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
courseRun: {
|
|
37
|
+
...courseRun,
|
|
38
|
+
title: 'Certificate Product',
|
|
39
|
+
price_currency: 'EUR',
|
|
40
|
+
offer: CourseOffer.FREE,
|
|
41
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
42
|
+
certificate_price: 100,
|
|
43
|
+
certificate_discounted_price: 80,
|
|
44
|
+
certificate_discount: '-20 €',
|
|
45
|
+
},
|
|
46
|
+
course: PacedCourseFactory().one(),
|
|
47
|
+
showLanguages: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const credentialSyllabusCourseRun: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
courseRun: {
|
|
53
|
+
...courseRun,
|
|
54
|
+
title: 'Certificate Product',
|
|
55
|
+
price_currency: 'EUR',
|
|
56
|
+
offer: CourseOffer.PAID,
|
|
57
|
+
price: 100,
|
|
58
|
+
certificate_offer: CourseCertificateOffer.FREE,
|
|
59
|
+
discounted_price: null,
|
|
60
|
+
discount: null,
|
|
61
|
+
},
|
|
62
|
+
course: PacedCourseFactory().one(),
|
|
63
|
+
showLanguages: true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export const credentialDiscountSyllabusCourseRun: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
courseRun: {
|
|
69
|
+
...courseRun,
|
|
70
|
+
title: 'Certificate Product',
|
|
71
|
+
price_currency: 'EUR',
|
|
72
|
+
offer: CourseOffer.PAID,
|
|
73
|
+
price: 100,
|
|
74
|
+
certificate_offer: CourseCertificateOffer.FREE,
|
|
75
|
+
discounted_price: 80,
|
|
76
|
+
discount: '-20 €',
|
|
77
|
+
},
|
|
78
|
+
course: PacedCourseFactory().one(),
|
|
79
|
+
showLanguages: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -128,6 +128,13 @@ const OpenedCourseRun = ({
|
|
|
128
128
|
currency: courseRun.price_currency,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
if ((courseRun.discounted_price ?? -1) >= 0) {
|
|
133
|
+
enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
|
|
134
|
+
style: 'currency',
|
|
135
|
+
currency: courseRun.price_currency,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
if (courseRun.certificate_offer) {
|
|
@@ -144,6 +151,13 @@ const OpenedCourseRun = ({
|
|
|
144
151
|
currency: courseRun.price_currency,
|
|
145
152
|
});
|
|
146
153
|
}
|
|
154
|
+
|
|
155
|
+
if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
|
|
156
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
|
|
157
|
+
style: 'currency',
|
|
158
|
+
currency: courseRun.price_currency,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
return (
|
|
@@ -119,6 +119,13 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
119
119
|
currency: courseRun.price_currency,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
if ((courseRun.discounted_price ?? -1) >= 0) {
|
|
124
|
+
enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
|
|
125
|
+
style: 'currency',
|
|
126
|
+
currency: courseRun.price_currency,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
if (courseRun.certificate_offer) {
|
|
@@ -135,6 +142,13 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
135
142
|
currency: courseRun.price_currency,
|
|
136
143
|
});
|
|
137
144
|
}
|
|
145
|
+
|
|
146
|
+
if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
|
|
147
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
|
|
148
|
+
style: 'currency',
|
|
149
|
+
currency: courseRun.price_currency,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
return (
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
import SyllabusCourseRunsList from 'widgets/SyllabusCourseRunsList/index';
|
|
23
23
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
24
24
|
import { CourseRun, Priority } from 'types';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { Offering } from 'types/Joanie';
|
|
26
|
+
import { OfferingFactory } from 'utils/test/factories/joanie';
|
|
27
27
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
28
28
|
import { StringHelper } from 'utils/StringHelper';
|
|
29
29
|
import { computeStates } from 'utils/CourseRuns';
|
|
@@ -211,9 +211,9 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
211
211
|
});
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
-
const expectCourseProduct = async (container: HTMLElement,
|
|
214
|
+
const expectCourseProduct = async (container: HTMLElement, offering: Offering) => {
|
|
215
215
|
const heading = await findByRole(container, 'heading', {
|
|
216
|
-
name:
|
|
216
|
+
name: offering.product.title,
|
|
217
217
|
});
|
|
218
218
|
expect(Array.from(heading.classList)).toContain('product-widget__title');
|
|
219
219
|
};
|
|
@@ -383,9 +383,9 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
383
383
|
|
|
384
384
|
it('has one opened product', async () => {
|
|
385
385
|
const course = PacedCourseFactory().one();
|
|
386
|
-
const
|
|
387
|
-
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${
|
|
388
|
-
fetchMock.get(resourceLink,
|
|
386
|
+
const offering = OfferingFactory().one();
|
|
387
|
+
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offering.product.id}/`;
|
|
388
|
+
fetchMock.get(resourceLink, offering);
|
|
389
389
|
|
|
390
390
|
const courseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
391
391
|
resource_link: resourceLink,
|
|
@@ -406,7 +406,7 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
406
406
|
expect(getHeaderContainer().querySelectorAll('.course-detail__run-descriptions').length).toBe(
|
|
407
407
|
1,
|
|
408
408
|
);
|
|
409
|
-
await expectCourseProduct(getHeaderContainer(),
|
|
409
|
+
await expectCourseProduct(getHeaderContainer(), offering);
|
|
410
410
|
|
|
411
411
|
// Portal.
|
|
412
412
|
expectEmptyPortalContainer();
|
|
@@ -1465,4 +1465,50 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
1465
1465
|
expect(content).not.toContain('The certification process is');
|
|
1466
1466
|
expect(content).not.toContain('<br>€59.99');
|
|
1467
1467
|
});
|
|
1468
|
+
|
|
1469
|
+
it('renders price discount on SyllabusCourseRun', async () => {
|
|
1470
|
+
const course = PacedCourseFactory().one();
|
|
1471
|
+
const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
1472
|
+
languages: ['en'],
|
|
1473
|
+
price_currency: 'EUR',
|
|
1474
|
+
offer: 'paid',
|
|
1475
|
+
price: 49.99,
|
|
1476
|
+
certificate_offer: undefined,
|
|
1477
|
+
certificate_price: undefined,
|
|
1478
|
+
discounted_price: 30.0,
|
|
1479
|
+
discount: '-20%',
|
|
1480
|
+
}).one();
|
|
1481
|
+
|
|
1482
|
+
render(
|
|
1483
|
+
<div className="course-detail__row course-detail__runs course-detail__runs--open">
|
|
1484
|
+
<SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
|
|
1485
|
+
</div>,
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
const content = getHeaderContainer().innerHTML;
|
|
1489
|
+
expect(content).toContain('<dd>Paid access<br>€30.00</dd>');
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('renders certificate discount on SyllabusCourseRun', async () => {
|
|
1493
|
+
const course = PacedCourseFactory().one();
|
|
1494
|
+
const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
1495
|
+
languages: ['en'],
|
|
1496
|
+
price_currency: 'EUR',
|
|
1497
|
+
offer: 'free',
|
|
1498
|
+
price: undefined,
|
|
1499
|
+
certificate_offer: 'paid',
|
|
1500
|
+
certificate_price: 100.0,
|
|
1501
|
+
certificate_discounted_price: 70.0,
|
|
1502
|
+
certificate_discount: '-30%',
|
|
1503
|
+
}).one();
|
|
1504
|
+
|
|
1505
|
+
render(
|
|
1506
|
+
<div className="course-detail__row course-detail__runs course-detail__runs--open">
|
|
1507
|
+
<SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
|
|
1508
|
+
</div>,
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
const content = getHeaderContainer().innerHTML;
|
|
1512
|
+
expect(content).toContain('<dd>Paid certificate<br>€70.00</dd>');
|
|
1513
|
+
});
|
|
1468
1514
|
});
|
package/package.json
CHANGED
|
@@ -396,6 +396,12 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
.offer_prices {
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-direction: column;
|
|
402
|
+
align-items: flex-end;
|
|
403
|
+
}
|
|
404
|
+
|
|
399
405
|
.offer__price {
|
|
400
406
|
$visibility: r-theme-val(course-glimpse, offer-price-visibility);
|
|
401
407
|
@if $visibility == hidden {
|
|
@@ -404,6 +410,16 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
|
|
|
404
410
|
visibility: $visibility;
|
|
405
411
|
// Align vertically the price with the icon
|
|
406
412
|
margin-top: calc(1ex - 1cap);
|
|
413
|
+
|
|
414
|
+
&--striked,
|
|
415
|
+
&--discounted {
|
|
416
|
+
display: inline-block;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
&--striked {
|
|
420
|
+
text-decoration: line-through;
|
|
421
|
+
opacity: 0.5;
|
|
422
|
+
}
|
|
407
423
|
}
|
|
408
424
|
}
|
|
409
425
|
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { defineMessages } from 'react-intl';
|
|
2
|
-
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
-
import { API, CourseProductRelation, CourseProductRelationQueryFilters } from 'types/Joanie';
|
|
4
|
-
import { useResource, useResources, UseResourcesProps } from 'hooks/useResources';
|
|
5
|
-
|
|
6
|
-
const messages = defineMessages({
|
|
7
|
-
errorGet: {
|
|
8
|
-
id: 'hooks.useCourseProductRelations.errorGet',
|
|
9
|
-
description:
|
|
10
|
-
'Error message shown to the user when course product relation fetch request fails.',
|
|
11
|
-
defaultMessage: 'An error occurred while fetching trainings. Please retry later.',
|
|
12
|
-
},
|
|
13
|
-
errorNotFound: {
|
|
14
|
-
id: 'hooks.useCourseProductRelations.errorNotFound',
|
|
15
|
-
description: 'Error message shown to the user when no course product relation matches.',
|
|
16
|
-
defaultMessage: 'Cannot find the training.',
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Joanie Api hook to retrieve/create/update/delete course
|
|
22
|
-
* owned by the authenticated user.
|
|
23
|
-
*/
|
|
24
|
-
const props: UseResourcesProps<
|
|
25
|
-
CourseProductRelation,
|
|
26
|
-
CourseProductRelationQueryFilters,
|
|
27
|
-
API['courseProductRelations']
|
|
28
|
-
> = {
|
|
29
|
-
queryKey: ['courseProductRelations'],
|
|
30
|
-
apiInterface: () => useJoanieApi().courseProductRelations,
|
|
31
|
-
session: true,
|
|
32
|
-
messages,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const useCourseProductRelations = useResources<
|
|
36
|
-
CourseProductRelation,
|
|
37
|
-
CourseProductRelationQueryFilters,
|
|
38
|
-
API['courseProductRelations']
|
|
39
|
-
>(props);
|
|
40
|
-
|
|
41
|
-
export const useCourseProductRelation = useResource<
|
|
42
|
-
CourseProductRelation,
|
|
43
|
-
CourseProductRelationQueryFilters
|
|
44
|
-
>(props);
|