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.
- package/js/api/joanie.ts +144 -0
- package/js/components/PaymentInterfaces/types.ts +7 -0
- package/js/components/PaymentScheduleGrid/index.tsx +4 -2
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +9 -2
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +33 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +253 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +314 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/StepContent.tsx +528 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +47 -271
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +25 -11
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +54 -6
- package/js/components/SaleTunnel/_styles.scss +55 -0
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +356 -0
- package/js/components/SaleTunnel/{index.full-process.spec.tsx → index.full-process-b2c.spec.tsx} +4 -1
- package/js/components/SaleTunnel/index.spec.tsx +104 -0
- package/js/hooks/useBatchOrder/index.tsx +36 -0
- package/js/hooks/useContractArchive/index.ts +2 -0
- package/js/hooks/useOfferingOrganizations/index.tsx +38 -0
- package/js/hooks/useOrganizationAgreements.tsx/index.tsx +66 -0
- package/js/hooks/useOrganizationQuotes/index.tsx +56 -0
- package/js/hooks/useTeacherPendingAgreementsCount/index.ts +34 -0
- package/js/pages/DashboardBatchOrderLayout/_styles.scss +5 -0
- package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +78 -0
- package/js/pages/DashboardBatchOrderLayout/index.tsx +45 -0
- package/js/pages/DashboardBatchOrders/index.spec.tsx +237 -0
- package/js/pages/DashboardBatchOrders/index.tsx +84 -0
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardCourseContractsLayout/index.tsx +0 -1
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +3 -1
- package/js/pages/TeacherDashboardOrganizationAgreements/AgreementActionsBar.tsx +49 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +79 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/OrganizationAgreementFrame.tsx +71 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +60 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useAgreementsAbilities.tsx +8 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useHasAgreementToDownload.tsx +27 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/hooks/useTeacherAgreementsToSign.tsx +32 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/index.spec.tsx +433 -0
- package/js/pages/TeacherDashboardOrganizationAgreements/index.tsx +130 -0
- package/js/pages/TeacherDashboardOrganizationAgreementsLayout/index.tsx +25 -0
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +9 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +40 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +194 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +144 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +521 -0
- package/js/pages/TeacherDashboardOrganizationQuotesLayout/index.tsx +26 -0
- package/js/types/Joanie.ts +216 -1
- package/js/utils/AbilitiesHelper/agreementAbilities.ts +14 -0
- package/js/utils/AbilitiesHelper/index.ts +7 -0
- package/js/utils/AbilitiesHelper/types.ts +12 -3
- package/js/utils/ObjectHelper/index.ts +20 -0
- package/js/utils/OrderHelper/index.ts +10 -0
- package/js/utils/test/factories/joanie.ts +156 -1
- package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/_styles.scss +14 -0
- package/js/widgets/Dashboard/components/DashboardBatchOrderLoader/index.tsx +32 -0
- package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +18 -0
- package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +25 -2
- package/js/widgets/Dashboard/components/DashboardCard/index.tsx +4 -2
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +88 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +216 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +316 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.spec.tsx +27 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +175 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +5 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +4 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/_styles.scss +5 -0
- package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +43 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.spec.tsx +214 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.tsx +47 -0
- package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +1 -0
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +21 -3
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +9 -0
- package/js/widgets/Dashboard/utils/learnerRoutes.tsx +30 -0
- package/js/widgets/Dashboard/utils/learnerRoutesPaths.tsx +12 -0
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +12 -0
- package/js/widgets/Dashboard/utils/teacherRoutes.tsx +17 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +8 -2
- package/package.json +4 -1
- package/scss/colors/_theme.scss +1 -1
- package/scss/components/_index.scss +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
4
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
5
|
+
import { render } from 'utils/test/render';
|
|
6
|
+
import { BatchOrderReadFactory } from 'utils/test/factories/joanie';
|
|
7
|
+
import { DashboardItemBatchOrder } from '.';
|
|
8
|
+
|
|
9
|
+
jest.mock('utils/context', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: mockRichieContextFactory({
|
|
12
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
13
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
14
|
+
}).one(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('<DashboardItemBatchOrder />', () => {
|
|
18
|
+
setupJoanieSession();
|
|
19
|
+
|
|
20
|
+
it('renders a batch order', async () => {
|
|
21
|
+
const batchOrder = BatchOrderReadFactory().one();
|
|
22
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/`, [batchOrder]);
|
|
23
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`, batchOrder);
|
|
24
|
+
render(<DashboardItemBatchOrder batchOrder={batchOrder} />);
|
|
25
|
+
expect(await screen.findByText(batchOrder.offering?.product.title)).toBeVisible();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
|
|
2
|
+
import { generatePath } from 'react-router';
|
|
3
|
+
import { BatchOrderRead, BatchOrderState } from 'types/Joanie';
|
|
4
|
+
import { PaymentMethod } from 'components/PaymentInterfaces/types';
|
|
5
|
+
import Badge from 'components/Badge';
|
|
6
|
+
import { DashboardItem } from 'widgets/Dashboard/components/DashboardItem/index';
|
|
7
|
+
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
8
|
+
import { RouterButton } from 'widgets/Dashboard/components/RouterButton';
|
|
9
|
+
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
10
|
+
import { DashboardBatchOrderSubItems } from './DashboardBatchOrderSubItems';
|
|
11
|
+
import { BatchOrderPaymentManager } from './BatchOrderPaymentModal/BatchOrderPaymentManager';
|
|
12
|
+
|
|
13
|
+
const messages = defineMessages({
|
|
14
|
+
seats: {
|
|
15
|
+
id: 'batchOrder.seats',
|
|
16
|
+
description: 'Text displayed for seats value in batch order',
|
|
17
|
+
defaultMessage: 'Seats',
|
|
18
|
+
},
|
|
19
|
+
[BatchOrderState.DRAFT]: {
|
|
20
|
+
id: 'batchOrder.status.draft',
|
|
21
|
+
description: 'Status label for a draft batch order',
|
|
22
|
+
defaultMessage: 'Draft',
|
|
23
|
+
},
|
|
24
|
+
[BatchOrderState.ASSIGNED]: {
|
|
25
|
+
id: 'batchOrder.status.assigned',
|
|
26
|
+
description: 'Status label for an assigned batch order',
|
|
27
|
+
defaultMessage: 'Assigned',
|
|
28
|
+
},
|
|
29
|
+
[BatchOrderState.QUOTED]: {
|
|
30
|
+
id: 'batchOrder.status.quoted',
|
|
31
|
+
description: 'Status label for a quoted batch order',
|
|
32
|
+
defaultMessage: 'Quoted',
|
|
33
|
+
},
|
|
34
|
+
[BatchOrderState.TO_SIGN]: {
|
|
35
|
+
id: 'batchOrder.status.to_sign',
|
|
36
|
+
description: 'Status label for a batch order awaiting signature',
|
|
37
|
+
defaultMessage: 'To sign',
|
|
38
|
+
},
|
|
39
|
+
[BatchOrderState.SIGNING]: {
|
|
40
|
+
id: 'batchOrder.status.signing',
|
|
41
|
+
description: 'Status label for a batch order in signing process',
|
|
42
|
+
defaultMessage: 'Signing',
|
|
43
|
+
},
|
|
44
|
+
[BatchOrderState.PENDING]: {
|
|
45
|
+
id: 'batchOrder.status.pending',
|
|
46
|
+
description: 'Status label for a pending batch order',
|
|
47
|
+
defaultMessage: 'Pending',
|
|
48
|
+
},
|
|
49
|
+
[BatchOrderState.PROCESS_PAYMENT]: {
|
|
50
|
+
id: 'batchOrder.status.processPayment',
|
|
51
|
+
description: 'Status label for a process payment batch order',
|
|
52
|
+
defaultMessage: 'Process payment',
|
|
53
|
+
},
|
|
54
|
+
[BatchOrderState.FAILED_PAYMENT]: {
|
|
55
|
+
id: 'batchOrder.status.failed_payment',
|
|
56
|
+
description: 'Status label for a batch order with failed payment',
|
|
57
|
+
defaultMessage: 'Failed payment',
|
|
58
|
+
},
|
|
59
|
+
[BatchOrderState.CANCELED]: {
|
|
60
|
+
id: 'batchOrder.status.canceled',
|
|
61
|
+
description: 'Status label for a canceled batch order',
|
|
62
|
+
defaultMessage: 'Canceled',
|
|
63
|
+
},
|
|
64
|
+
[BatchOrderState.COMPLETED]: {
|
|
65
|
+
id: 'batchOrder.status.completed',
|
|
66
|
+
description: 'Status label for a completed batch order',
|
|
67
|
+
defaultMessage: 'Completed',
|
|
68
|
+
},
|
|
69
|
+
[PaymentMethod.BANK_TRANSFER]: {
|
|
70
|
+
id: 'batchOrder.payment.bank',
|
|
71
|
+
description: 'Label for bank transfer payment method',
|
|
72
|
+
defaultMessage: 'Bank transfer',
|
|
73
|
+
},
|
|
74
|
+
[PaymentMethod.CARD_PAYMENT]: {
|
|
75
|
+
id: 'batchOrder.payment.card',
|
|
76
|
+
description: 'Label for card payment method',
|
|
77
|
+
defaultMessage: 'Card payment',
|
|
78
|
+
},
|
|
79
|
+
[PaymentMethod.PURCHASE_ORDER]: {
|
|
80
|
+
id: 'batchOrder.payment.order',
|
|
81
|
+
description: 'Label for purchase order payment method',
|
|
82
|
+
defaultMessage: 'Purchase order',
|
|
83
|
+
},
|
|
84
|
+
paymentNeededButton: {
|
|
85
|
+
id: 'components.ProductCertificateFooter.paymentNeededButton',
|
|
86
|
+
description: 'Button label for the payment needed message',
|
|
87
|
+
defaultMessage: 'Pay {amount}',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const DashboardItemBatchOrder = ({
|
|
92
|
+
batchOrder,
|
|
93
|
+
showDetails = false,
|
|
94
|
+
}: {
|
|
95
|
+
batchOrder: BatchOrderRead;
|
|
96
|
+
showDetails?: boolean;
|
|
97
|
+
}) => {
|
|
98
|
+
const intl = useIntl();
|
|
99
|
+
const needsPayment =
|
|
100
|
+
(batchOrder.state === BatchOrderState.PENDING ||
|
|
101
|
+
batchOrder.state === BatchOrderState.PROCESS_PAYMENT) &&
|
|
102
|
+
batchOrder.payment_method === PaymentMethod.CARD_PAYMENT;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="dashboard-item-order">
|
|
106
|
+
<DashboardItem
|
|
107
|
+
data-testid={`dashboard-item-batch-order-${batchOrder.id}`}
|
|
108
|
+
title={batchOrder.offering?.product.title}
|
|
109
|
+
code={`Ref. ${batchOrder.id}`}
|
|
110
|
+
imageUrl={batchOrder.offering?.course.cover?.src}
|
|
111
|
+
footer={
|
|
112
|
+
<div className="dashboard-item-order__footer">
|
|
113
|
+
<div className="dashboard-item__block__status">
|
|
114
|
+
{batchOrder.state && (
|
|
115
|
+
<Badge color="primary">
|
|
116
|
+
<div className="dashboard-item__block__status__badge">
|
|
117
|
+
<FormattedMessage {...messages[batchOrder.state]} />
|
|
118
|
+
</div>
|
|
119
|
+
</Badge>
|
|
120
|
+
)}
|
|
121
|
+
{batchOrder.nb_seats && (
|
|
122
|
+
<div className="dashboard-item__block__information">
|
|
123
|
+
<Icon name={IconTypeEnum.GROUPS} size="small" />
|
|
124
|
+
<span>{batchOrder.nb_seats}</span>
|
|
125
|
+
<span>{intl.formatMessage(messages.seats)}</span>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{batchOrder.payment_method && (
|
|
129
|
+
<div className="dashboard-item__block__information">
|
|
130
|
+
<Icon name={IconTypeEnum.MONEY} size="small" />
|
|
131
|
+
<FormattedMessage {...messages[batchOrder.payment_method]} />
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
{showDetails && (
|
|
135
|
+
<div className="dashboard-item__block__information">
|
|
136
|
+
<Icon name={IconTypeEnum.OFFER_SUBSCRIPTION} size="small" />
|
|
137
|
+
<span>
|
|
138
|
+
<FormattedNumber
|
|
139
|
+
value={batchOrder.total}
|
|
140
|
+
currency={batchOrder.currency}
|
|
141
|
+
style="currency"
|
|
142
|
+
/>
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
<RouterButton
|
|
148
|
+
size="small"
|
|
149
|
+
className="dashboard-item__button"
|
|
150
|
+
href={
|
|
151
|
+
showDetails
|
|
152
|
+
? generatePath(LearnerDashboardPaths.BATCH_ORDERS, {
|
|
153
|
+
batchOrderId: batchOrder.id!,
|
|
154
|
+
})
|
|
155
|
+
: generatePath(LearnerDashboardPaths.BATCH_ORDER, {
|
|
156
|
+
batchOrderId: batchOrder.id!,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
data-testid="dashboard-item-batch-order__button"
|
|
160
|
+
>
|
|
161
|
+
{intl.formatMessage(
|
|
162
|
+
showDetails
|
|
163
|
+
? { id: 'batchOrder.viewAll', defaultMessage: 'View all batch orders' }
|
|
164
|
+
: { id: 'batchOrder.viewOne', defaultMessage: 'View details' },
|
|
165
|
+
)}
|
|
166
|
+
</RouterButton>
|
|
167
|
+
</div>
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
{needsPayment && <BatchOrderPaymentManager batchOrder={batchOrder} />}
|
|
171
|
+
{showDetails && <DashboardBatchOrderSubItems batchOrder={batchOrder} />}
|
|
172
|
+
</DashboardItem>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
@@ -78,6 +78,7 @@ export const DashboardItemOrder = ({
|
|
|
78
78
|
const isProductPurchasable = ProductHelper.isPurchasable(offering?.product);
|
|
79
79
|
const isNotResumable = !isActive && !isProductPurchasable;
|
|
80
80
|
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
81
|
+
const isFreeFromBatchOrder = OrderHelper.isFreeFromBatchOrder(order);
|
|
81
82
|
|
|
82
83
|
if (!product) return null;
|
|
83
84
|
|
|
@@ -111,7 +112,7 @@ export const DashboardItemOrder = ({
|
|
|
111
112
|
</>
|
|
112
113
|
)}
|
|
113
114
|
</div>
|
|
114
|
-
{!isNotResumable && showDetailsButton && (
|
|
115
|
+
{!isNotResumable && showDetailsButton && !isFreeFromBatchOrder && (
|
|
115
116
|
<RouterButton
|
|
116
117
|
size="small"
|
|
117
118
|
className="dashboard-item__button"
|
|
@@ -199,7 +200,9 @@ export const DashboardItemOrder = ({
|
|
|
199
200
|
{showCertificate && !!product?.certificate_definition && (
|
|
200
201
|
<CertificateItem order={order} product={product} />
|
|
201
202
|
)}
|
|
202
|
-
{writable &&
|
|
203
|
+
{writable && !isFreeFromBatchOrder && (
|
|
204
|
+
<OrganizationBlock order={order} product={product} />
|
|
205
|
+
)}
|
|
203
206
|
</>
|
|
204
207
|
)}
|
|
205
208
|
</div>
|
|
@@ -2,6 +2,7 @@ import { defineMessages, FormattedMessage } from 'react-intl';
|
|
|
2
2
|
import { Button } from '@openfun/cunningham-react';
|
|
3
3
|
import { CredentialOrder, Product } from 'types/Joanie';
|
|
4
4
|
import { AddressView } from 'components/Address';
|
|
5
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
5
6
|
import ContractItem from '../ContractItem';
|
|
6
7
|
import Installment from '../Installment';
|
|
7
8
|
|
|
@@ -64,6 +65,8 @@ const OrganizationBlock = ({ order, product }: Props) => {
|
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
const hidePaymentBlock =
|
|
69
|
+
OrderHelper.isFreeWithVoucher(order) || OrderHelper.isFreeFromBatchOrder(order);
|
|
67
70
|
const showContactsBlock =
|
|
68
71
|
organization.contact_email || organization.contact_phone || organization.dpo_email;
|
|
69
72
|
|
|
@@ -141,7 +144,7 @@ const OrganizationBlock = ({ order, product }: Props) => {
|
|
|
141
144
|
</div>
|
|
142
145
|
</div>
|
|
143
146
|
)}
|
|
144
|
-
<Installment order={order} />
|
|
147
|
+
{!hidePaymentBlock && <Installment order={order} />}
|
|
145
148
|
</div>
|
|
146
149
|
</div>
|
|
147
150
|
);
|
|
@@ -62,6 +62,34 @@
|
|
|
62
62
|
@include media-breakpoint-down(sm) {
|
|
63
63
|
margin-bottom: 0.5rem;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
&__badge {
|
|
67
|
+
padding: 0.25rem;
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: row;
|
|
70
|
+
align-items: center;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&__details {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
font-size: 1rem;
|
|
77
|
+
&__title {
|
|
78
|
+
font-weight: bold;
|
|
79
|
+
margin-top: 0.5rem;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&__information {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: row;
|
|
87
|
+
gap: 0.25rem;
|
|
88
|
+
margin: 0 0.5rem;
|
|
89
|
+
text-align: center;
|
|
90
|
+
align-items: center;
|
|
91
|
+
align-content: center;
|
|
92
|
+
justify-content: center;
|
|
65
93
|
}
|
|
66
94
|
}
|
|
67
95
|
|
|
@@ -69,6 +97,11 @@
|
|
|
69
97
|
justify-content: center;
|
|
70
98
|
min-width: rem-calc(140px);
|
|
71
99
|
}
|
|
100
|
+
|
|
101
|
+
&__label {
|
|
102
|
+
font-weight: bold;
|
|
103
|
+
margin-right: 0.25rem;
|
|
104
|
+
}
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
.dashboard-sub-item-list {
|
|
@@ -123,6 +156,16 @@
|
|
|
123
156
|
overflow: hidden;
|
|
124
157
|
}
|
|
125
158
|
}
|
|
159
|
+
|
|
160
|
+
&__footer {
|
|
161
|
+
.content {
|
|
162
|
+
padding: 0.5rem 1rem;
|
|
163
|
+
font-size: 0.8rem;
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 1rem;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
.dashboard-item__course-enrolling {
|
package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.spec.tsx
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { screen } from '@testing-library/react';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { faker } from '@faker-js/faker';
|
|
4
|
+
import queryString from 'query-string';
|
|
5
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
6
|
+
import { PER_PAGE } from 'settings';
|
|
7
|
+
import { ContractResourceQuery, ContractState } from 'types/Joanie';
|
|
8
|
+
import { AgreementFactory } from 'utils/test/factories/joanie';
|
|
9
|
+
import { AgreementActions } from 'utils/AbilitiesHelper/types';
|
|
10
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
11
|
+
import { render } from 'utils/test/render';
|
|
12
|
+
import { MenuLink } from '../..';
|
|
13
|
+
import AgreementNavLink from '.';
|
|
14
|
+
|
|
15
|
+
jest.mock('utils/context', () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: mockRichieContextFactory({
|
|
18
|
+
authentication: { backend: 'fonzie', endpoint: 'https://demo.endpoint' },
|
|
19
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
20
|
+
}).one(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('<AgreementNavLink />', () => {
|
|
24
|
+
setupJoanieSession();
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// useDefaultOrganization hook request organization list
|
|
28
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', []);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should render a AgreementNavLink with route and label when neither organizationId and offeringId are given', () => {
|
|
32
|
+
const link: MenuLink = {
|
|
33
|
+
to: '/dummy/url/',
|
|
34
|
+
label: 'My agreement navigation link',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
render(<AgreementNavLink link={link} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByRole('link', { name: 'My agreement navigation link' })).toBeInTheDocument();
|
|
40
|
+
expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('without sign ability', () => {
|
|
44
|
+
const agreementAbilities = { [AgreementActions.SIGN]: false };
|
|
45
|
+
it.each([
|
|
46
|
+
{
|
|
47
|
+
organizationId: faker.string.uuid(),
|
|
48
|
+
offeringId: undefined,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
organizationId: faker.string.uuid(),
|
|
52
|
+
offeringId: faker.string.uuid(),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
organizationId: undefined,
|
|
56
|
+
offeringId: faker.string.uuid(),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
organizationId: undefined,
|
|
60
|
+
offeringId: undefined,
|
|
61
|
+
},
|
|
62
|
+
])(
|
|
63
|
+
'should never render Badge for organizationId: $organizationId and offeringId: $offeringId',
|
|
64
|
+
async ({ organizationId, offeringId }) => {
|
|
65
|
+
let agreementQueryParams: ContractResourceQuery = {
|
|
66
|
+
signature_state: ContractState.LEARNER_SIGNED,
|
|
67
|
+
page: 1,
|
|
68
|
+
page_size: PER_PAGE.teacherContractList,
|
|
69
|
+
};
|
|
70
|
+
if (offeringId) {
|
|
71
|
+
agreementQueryParams = {
|
|
72
|
+
offering_id: offeringId,
|
|
73
|
+
...agreementQueryParams,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fetchMock.get(
|
|
78
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organizationId}/agreements/?${queryString.stringify(
|
|
79
|
+
agreementQueryParams,
|
|
80
|
+
{ sort: false },
|
|
81
|
+
)}`,
|
|
82
|
+
{
|
|
83
|
+
count: 1,
|
|
84
|
+
next: null,
|
|
85
|
+
previous: null,
|
|
86
|
+
results: [AgreementFactory({ abilities: agreementAbilities }).one()],
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<AgreementNavLink
|
|
92
|
+
link={{
|
|
93
|
+
to: '/dummy/url/',
|
|
94
|
+
label: 'My agreement navigation link',
|
|
95
|
+
}}
|
|
96
|
+
organizationId={organizationId}
|
|
97
|
+
offeringId={offeringId}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
screen.getByRole('link', { name: 'My agreement navigation link' }),
|
|
103
|
+
).toBeInTheDocument();
|
|
104
|
+
expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('with sign ability', () => {
|
|
110
|
+
const agreementAbilities = { [AgreementActions.SIGN]: true };
|
|
111
|
+
it.each([
|
|
112
|
+
// with 1 agreements to sign
|
|
113
|
+
{
|
|
114
|
+
organizationId: faker.string.uuid(),
|
|
115
|
+
offeringId: undefined,
|
|
116
|
+
nbAgreementsToSign: 1,
|
|
117
|
+
expectedBadgeCount: 1,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
organizationId: faker.string.uuid(),
|
|
121
|
+
offeringId: faker.string.uuid(),
|
|
122
|
+
nbAgreementsToSign: 1,
|
|
123
|
+
expectedBadgeCount: 1,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
organizationId: undefined,
|
|
127
|
+
offeringId: faker.string.uuid(),
|
|
128
|
+
nbAgreementsToSign: 1,
|
|
129
|
+
expectedBadgeCount: undefined,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
organizationId: undefined,
|
|
133
|
+
offeringId: undefined,
|
|
134
|
+
nbAgreementsToSign: 1,
|
|
135
|
+
expectedBadgeCount: undefined,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// with 0 agreements to sign
|
|
139
|
+
{
|
|
140
|
+
organizationId: faker.string.uuid(),
|
|
141
|
+
offeringId: undefined,
|
|
142
|
+
nbAgreementsToSign: 0,
|
|
143
|
+
expectedBadgeCount: undefined,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
organizationId: faker.string.uuid(),
|
|
147
|
+
offeringId: faker.string.uuid(),
|
|
148
|
+
nbAgreementsToSign: 0,
|
|
149
|
+
expectedBadgeCount: undefined,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
organizationId: undefined,
|
|
153
|
+
offeringId: faker.string.uuid(),
|
|
154
|
+
nbAgreementsToSign: 0,
|
|
155
|
+
expectedBadgeCount: undefined,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
organizationId: undefined,
|
|
159
|
+
offeringId: undefined,
|
|
160
|
+
nbAgreementsToSign: 0,
|
|
161
|
+
expectedBadgeCount: undefined,
|
|
162
|
+
},
|
|
163
|
+
])(
|
|
164
|
+
'should render Badge (count: $expectedBadgeCount) for nb agreements to sign: $nbAgreementsToSign, organizationId: $organizationId and offeringId: $offeringId',
|
|
165
|
+
async ({ nbAgreementsToSign, organizationId, offeringId, expectedBadgeCount }) => {
|
|
166
|
+
let agreementQueryParams: ContractResourceQuery = {
|
|
167
|
+
signature_state: ContractState.LEARNER_SIGNED,
|
|
168
|
+
page: 1,
|
|
169
|
+
page_size: PER_PAGE.teacherContractList,
|
|
170
|
+
};
|
|
171
|
+
if (offeringId) {
|
|
172
|
+
agreementQueryParams = {
|
|
173
|
+
offering_id: offeringId,
|
|
174
|
+
...agreementQueryParams,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fetchMock.get(
|
|
179
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organizationId}/agreements/?${queryString.stringify(
|
|
180
|
+
agreementQueryParams,
|
|
181
|
+
{ sort: false },
|
|
182
|
+
)}`,
|
|
183
|
+
{
|
|
184
|
+
count: nbAgreementsToSign,
|
|
185
|
+
next: null,
|
|
186
|
+
previous: null,
|
|
187
|
+
results: [AgreementFactory({ abilities: agreementAbilities }).one()],
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
render(
|
|
191
|
+
<AgreementNavLink
|
|
192
|
+
link={{
|
|
193
|
+
to: '/dummy/url/',
|
|
194
|
+
label: 'My agreement navigation link',
|
|
195
|
+
}}
|
|
196
|
+
organizationId={organizationId}
|
|
197
|
+
offeringId={offeringId}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
screen.getByRole('link', { name: 'My agreement navigation link' }),
|
|
203
|
+
).toBeInTheDocument();
|
|
204
|
+
if (expectedBadgeCount === undefined) {
|
|
205
|
+
expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
|
|
206
|
+
} else {
|
|
207
|
+
const $badge = await screen.findByTestId('badge');
|
|
208
|
+
expect($badge).toBeInTheDocument();
|
|
209
|
+
expect($badge).toHaveTextContent(`${expectedBadgeCount}`);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
package/js/widgets/Dashboard/components/DashboardSidebar/components/AgreementNavLink/index.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createSearchParams } from 'react-router';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
|
|
4
|
+
import { ContractState, Offering, Organization } from 'types/Joanie';
|
|
5
|
+
import { AgreementActions } from 'utils/AbilitiesHelper/types';
|
|
6
|
+
import useDefaultOrganizationId from 'hooks/useDefaultOrganizationId';
|
|
7
|
+
import useAgreementAbilities from 'pages/TeacherDashboardOrganizationAgreements/hooks/useAgreementsAbilities';
|
|
8
|
+
import useTeacherPendingAgreementsCount from 'hooks/useTeacherPendingAgreementsCount';
|
|
9
|
+
import MenuNavLink from '../MenuNavLink';
|
|
10
|
+
|
|
11
|
+
interface AgreementNavLinkProps {
|
|
12
|
+
link: MenuLink;
|
|
13
|
+
organizationId?: Organization['id'];
|
|
14
|
+
offeringId?: Offering['id'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const AgreementNavLink = ({ link, organizationId, offeringId }: AgreementNavLinkProps) => {
|
|
18
|
+
const defaultOrganizationId = useDefaultOrganizationId();
|
|
19
|
+
const { agreements: pendingAgreements, pendingAgreementCount } = useTeacherPendingAgreementsCount(
|
|
20
|
+
{
|
|
21
|
+
organizationId: organizationId || defaultOrganizationId,
|
|
22
|
+
offeringId,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
const agreementAbilities = useAgreementAbilities(pendingAgreements);
|
|
26
|
+
const canSignAgreements = agreementAbilities.can(AgreementActions.SIGN);
|
|
27
|
+
const hasAgreementsToSign = useMemo(
|
|
28
|
+
() => canSignAgreements && pendingAgreementCount > 0,
|
|
29
|
+
[canSignAgreements, pendingAgreementCount],
|
|
30
|
+
);
|
|
31
|
+
const searchParams = useMemo(() => {
|
|
32
|
+
if (hasAgreementsToSign) {
|
|
33
|
+
return createSearchParams({ signature_state: ContractState.LEARNER_SIGNED });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return createSearchParams({ signature_state: ContractState.SIGNED });
|
|
37
|
+
}, [hasAgreementsToSign]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<MenuNavLink
|
|
41
|
+
link={{ ...link, to: `${link.to}?${searchParams.toString()}` }}
|
|
42
|
+
badgeCount={hasAgreementsToSign ? pendingAgreementCount : undefined}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default AgreementNavLink;
|
|
@@ -37,6 +37,7 @@ export const LearnerDashboardSidebar = (props: Partial<DashboardSidebarProps>) =
|
|
|
37
37
|
LearnerDashboardPaths.CERTIFICATES,
|
|
38
38
|
LearnerDashboardPaths.CONTRACTS,
|
|
39
39
|
LearnerDashboardPaths.PREFERENCES,
|
|
40
|
+
LearnerDashboardPaths.BATCH_ORDERS,
|
|
40
41
|
].map((path) => ({
|
|
41
42
|
to: generatePath(path),
|
|
42
43
|
label: getRouteLabel(path),
|
|
@@ -34,6 +34,12 @@ describe('<TeacherDashboardOrganizationSidebar />', () => {
|
|
|
34
34
|
`/contracts/?signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
35
35
|
{ results: [], count: 0, previous: null, next: null },
|
|
36
36
|
);
|
|
37
|
+
fetchMock.get(
|
|
38
|
+
'https://joanie.endpoint/api/v1.0/organizations/' +
|
|
39
|
+
organization.id +
|
|
40
|
+
`/agreements/?signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
41
|
+
{ results: [], count: 0, previous: null, next: null },
|
|
42
|
+
);
|
|
37
43
|
|
|
38
44
|
render(<TeacherDashboardOrganizationSidebar />, {
|
|
39
45
|
routerOptions: { path: '/:organizationId', initialEntries: [`/${organization.id}`] },
|
|
@@ -50,9 +56,11 @@ describe('<TeacherDashboardOrganizationSidebar />', () => {
|
|
|
50
56
|
|
|
51
57
|
// It should display menu links
|
|
52
58
|
const links = screen.getAllByRole('link');
|
|
53
|
-
expect(links).toHaveLength(
|
|
59
|
+
expect(links).toHaveLength(4);
|
|
54
60
|
expect(links[0]).toHaveTextContent('Courses');
|
|
55
61
|
expect(links[1]).toHaveTextContent('Contracts');
|
|
62
|
+
expect(links[2]).toHaveTextContent('Quotes');
|
|
63
|
+
expect(links[3]).toHaveTextContent('Agreements');
|
|
56
64
|
// No badge should be displayed next to contract link
|
|
57
65
|
expect(links[1].nextSibling).toBeNull();
|
|
58
66
|
});
|
|
@@ -76,6 +84,12 @@ describe('<TeacherDashboardOrganizationSidebar />', () => {
|
|
|
76
84
|
next: null,
|
|
77
85
|
},
|
|
78
86
|
);
|
|
87
|
+
fetchMock.get(
|
|
88
|
+
'https://joanie.endpoint/api/v1.0/organizations/' +
|
|
89
|
+
organization.id +
|
|
90
|
+
`/agreements/?signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
91
|
+
{ results: [], count: 0, previous: null, next: null },
|
|
92
|
+
);
|
|
79
93
|
|
|
80
94
|
render(<TeacherDashboardOrganizationSidebar />, {
|
|
81
95
|
routerOptions: { path: '/:organizationId', initialEntries: [`/${organization.id}`] },
|
|
@@ -108,10 +122,14 @@ describe('<TeacherDashboardOrganizationSidebar />', () => {
|
|
|
108
122
|
{
|
|
109
123
|
results: ContractFactory({ abilities: { sign: false } }).many(contractToSignCount),
|
|
110
124
|
count: contractToSignCount,
|
|
111
|
-
previous: null,
|
|
112
|
-
next: null,
|
|
113
125
|
},
|
|
114
126
|
);
|
|
127
|
+
fetchMock.get(
|
|
128
|
+
'https://joanie.endpoint/api/v1.0/organizations/' +
|
|
129
|
+
organization.id +
|
|
130
|
+
`/agreements/?signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
131
|
+
{ results: [], count: 0, previous: null, next: null },
|
|
132
|
+
);
|
|
115
133
|
|
|
116
134
|
render(<TeacherDashboardOrganizationSidebar />, {
|
|
117
135
|
routerOptions: { path: '/:organizationId', initialEntries: [`/${organization.id}`] },
|
|
@@ -7,6 +7,7 @@ import { getDashboardRouteLabel } from 'widgets/Dashboard/utils/dashboardRoutes'
|
|
|
7
7
|
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
|
|
8
8
|
import { DashboardAvatar, DashboardAvatarVariantEnum } from '../DashboardAvatar';
|
|
9
9
|
import ContractNavLink from '../DashboardSidebar/components/ContractNavLink';
|
|
10
|
+
import AgreementNavLink from '../DashboardSidebar/components/AgreementNavLink';
|
|
10
11
|
|
|
11
12
|
const messages = defineMessages({
|
|
12
13
|
subHeader: {
|
|
@@ -47,12 +48,20 @@ export const TeacherDashboardOrganizationSidebar = () => {
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
if (basePath === TeacherDashboardPaths.ORGANIZATION_AGREEMENTS) {
|
|
52
|
+
menuLink.component = (
|
|
53
|
+
<AgreementNavLink link={menuLink} organizationId={organizationId} offeringId={offeringId} />
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
return menuLink;
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
const links = [
|
|
54
61
|
TeacherDashboardPaths.ORGANIZATION_COURSES,
|
|
55
62
|
TeacherDashboardPaths.ORGANIZATION_CONTRACTS,
|
|
63
|
+
TeacherDashboardPaths.ORGANIZATION_QUOTES,
|
|
64
|
+
TeacherDashboardPaths.ORGANIZATION_AGREEMENTS,
|
|
56
65
|
].map(getMenuLinkFromPath);
|
|
57
66
|
|
|
58
67
|
if (fetching) {
|