richie-education 3.4.0 → 3.4.1-dev13
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/main.js +11 -12
- package/js/api/joanie.ts +20 -0
- package/js/api/utils.ts +4 -3
- package/js/components/DownloadAgreementButton/index.tsx +51 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.spec.tsx +46 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.tsx +80 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +1 -1
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +1 -1
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +2 -1
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -5
- package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +1 -1
- package/js/hooks/useBatchOrder/index.tsx +21 -1
- package/js/hooks/useDownloadAgreement/index.spec.tsx +136 -0
- package/js/hooks/useDownloadAgreement/index.tsx +25 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +132 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.tsx +24 -0
- package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +19 -2
- package/js/pages/TeacherDashboardOrganizationQuotes/BatchOrderSeatInfoQuote.tsx +112 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +17 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +5 -2
- package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +7 -3
- package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +38 -26
- package/js/types/Joanie.ts +21 -1
- package/js/utils/download.ts +3 -1
- package/js/utils/test/factories/joanie.ts +15 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderAgreementInfo.tsx +72 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.spec.tsx +114 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +133 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +17 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts +24 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +16 -3
- package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +6 -2
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +2 -2
- package/js/widgets/Slider/index.tsx +7 -6
- package/package.json +2 -7
- package/scss/components/templates/richie/slider/_slider.scss +1 -1
- package/scss/objects/_course_glimpses.scss +1 -0
- package/scss/objects/_dashboard.scss +77 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
8
|
+
import { useDownloadBatchOrderSeats } from 'hooks/useDownloadBatchOrderSeats/index';
|
|
9
|
+
import { handle } from 'utils/errors/handle';
|
|
10
|
+
import { SessionProvider } from 'contexts/SessionContext';
|
|
11
|
+
import { Deferred } from 'utils/test/deferred';
|
|
12
|
+
import { BatchOrderReadFactory } from 'utils/test/factories/joanie';
|
|
13
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
14
|
+
|
|
15
|
+
jest.mock('utils/errors/handle');
|
|
16
|
+
jest.mock('utils/context', () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: mockRichieContextFactory({
|
|
19
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
20
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
21
|
+
}).one(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockHandle = handle as jest.MockedFn<typeof handle>;
|
|
25
|
+
|
|
26
|
+
describe('useDownloadBatchOrderSeats', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
29
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
30
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
// eslint-disable-next-line compat/compat
|
|
35
|
+
URL.createObjectURL = jest.fn();
|
|
36
|
+
// eslint-disable-next-line compat/compat
|
|
37
|
+
URL.revokeObjectURL = jest.fn();
|
|
38
|
+
HTMLAnchorElement.prototype.click = jest.fn();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
fetchMock.restore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
47
|
+
return (
|
|
48
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
49
|
+
<IntlProvider locale="en">
|
|
50
|
+
<SessionProvider>{children}</SessionProvider>
|
|
51
|
+
</IntlProvider>
|
|
52
|
+
</QueryClientProvider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
it('downloads the batch order seats CSV', async () => {
|
|
57
|
+
const batchOrder = BatchOrderReadFactory().one();
|
|
58
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/batch-orders/${batchOrder.id}/seats-export/`;
|
|
59
|
+
const deferred = new Deferred();
|
|
60
|
+
fetchMock.get(DOWNLOAD_URL, deferred.promise);
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useDownloadBatchOrderSeats(), {
|
|
63
|
+
wrapper: Wrapper,
|
|
64
|
+
});
|
|
65
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
66
|
+
|
|
67
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
68
|
+
// eslint-disable-next-line compat/compat
|
|
69
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
70
|
+
// eslint-disable-next-line compat/compat
|
|
71
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
72
|
+
expect(result.current.loading).toBe(false);
|
|
73
|
+
|
|
74
|
+
act(() => {
|
|
75
|
+
result.current.download(batchOrder.id);
|
|
76
|
+
});
|
|
77
|
+
expect(result.current.loading).toBe(true);
|
|
78
|
+
|
|
79
|
+
deferred.resolve({
|
|
80
|
+
status: HttpStatusCode.OK,
|
|
81
|
+
body: new Blob(['last_name,first_name,email']),
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'text/csv',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
89
|
+
// eslint-disable-next-line compat/compat
|
|
90
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
91
|
+
// eslint-disable-next-line compat/compat
|
|
92
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
93
|
+
expect(result.current.loading).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
fireEvent.blur(window);
|
|
97
|
+
// eslint-disable-next-line compat/compat
|
|
98
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles an error if seats export request fails', async () => {
|
|
102
|
+
const batchOrder = BatchOrderReadFactory().one();
|
|
103
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/batch-orders/${batchOrder.id}/seats-export/`;
|
|
104
|
+
fetchMock.get(DOWNLOAD_URL, HttpStatusCode.UNAUTHORIZED);
|
|
105
|
+
|
|
106
|
+
const { result } = renderHook(() => useDownloadBatchOrderSeats(), {
|
|
107
|
+
wrapper: Wrapper,
|
|
108
|
+
});
|
|
109
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
110
|
+
|
|
111
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
112
|
+
expect(mockHandle).not.toHaveBeenCalled();
|
|
113
|
+
// eslint-disable-next-line compat/compat
|
|
114
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
115
|
+
// eslint-disable-next-line compat/compat
|
|
116
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.download(batchOrder.id);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
124
|
+
expect(mockHandle).toHaveBeenNthCalledWith(1, new Error('Unauthorized'));
|
|
125
|
+
// eslint-disable-next-line compat/compat
|
|
126
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
127
|
+
// eslint-disable-next-line compat/compat
|
|
128
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
129
|
+
expect(result.current.loading).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
+
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
|
+
|
|
5
|
+
export const useDownloadBatchOrderSeats = () => {
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const API = useJoanieApi();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
download: async (batchOrderId: string, filename?: string) => {
|
|
11
|
+
setLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
await browserDownloadFromBlob(
|
|
14
|
+
() => API.user.batchOrders.seats_export(batchOrderId),
|
|
15
|
+
false,
|
|
16
|
+
filename,
|
|
17
|
+
);
|
|
18
|
+
} finally {
|
|
19
|
+
setLoading(false);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
loading,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -2,7 +2,11 @@ import { findByRole, render, screen, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { generatePath } from 'react-router';
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AgreementFactory,
|
|
7
|
+
BatchOrderReadFactory,
|
|
8
|
+
BatchOrderSeatFactory,
|
|
9
|
+
} from 'utils/test/factories/joanie';
|
|
6
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
7
11
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
8
12
|
import { expectUrlMatchLocationDisplayed } from 'utils/test/expectUrlMatchLocationDisplayed';
|
|
@@ -49,8 +53,21 @@ describe('<DashboardBatchOrderLayout />', () => {
|
|
|
49
53
|
|
|
50
54
|
it('renders sidebar', async () => {
|
|
51
55
|
const batchOrder = BatchOrderReadFactory().one();
|
|
52
|
-
|
|
56
|
+
const seats = BatchOrderSeatFactory().many(2);
|
|
53
57
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`, batchOrder);
|
|
58
|
+
fetchMock.get(
|
|
59
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/?page=1&page_size=10`,
|
|
60
|
+
{
|
|
61
|
+
results: seats,
|
|
62
|
+
count: seats.length,
|
|
63
|
+
previous: null,
|
|
64
|
+
next: null,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
fetchMock.get(
|
|
68
|
+
`https://joanie.endpoint/api/v1.0/organizations/${batchOrder.organization.id}/agreements/${batchOrder.contract_id}/`,
|
|
69
|
+
AgreementFactory().one(),
|
|
70
|
+
);
|
|
54
71
|
|
|
55
72
|
render(
|
|
56
73
|
WrapperWithDashboard(
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { FormattedMessage, useIntl } from 'react-intl';
|
|
3
|
+
import { Button, Input } from '@openfun/cunningham-react';
|
|
4
|
+
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
5
|
+
import Banner, { BannerType } from 'components/Banner';
|
|
6
|
+
import { useBatchOrderSeats } from 'hooks/useBatchOrder';
|
|
7
|
+
import { BatchOrderQuote, BatchOrderSeat } from 'types/Joanie';
|
|
8
|
+
import { batchOrderSeatInfoMessages } from 'widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages';
|
|
9
|
+
|
|
10
|
+
const ITEMS_PER_PAGE = 10;
|
|
11
|
+
|
|
12
|
+
export const BatchOrderSeatInfoQuote = ({ batchOrder }: { batchOrder: BatchOrderQuote }) => {
|
|
13
|
+
const intl = useIntl();
|
|
14
|
+
const [query, setQuery] = useState('');
|
|
15
|
+
const [page, setPage] = useState(1);
|
|
16
|
+
const [allSeats, setAllSeats] = useState<BatchOrderSeat[]>([]);
|
|
17
|
+
|
|
18
|
+
const seatsOwnedCount = batchOrder.seats_owned ?? 0;
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
items: seats,
|
|
22
|
+
meta,
|
|
23
|
+
states,
|
|
24
|
+
} = useBatchOrderSeats(
|
|
25
|
+
{
|
|
26
|
+
batch_order_id: batchOrder.id,
|
|
27
|
+
query: query || undefined,
|
|
28
|
+
page,
|
|
29
|
+
page_size: ITEMS_PER_PAGE,
|
|
30
|
+
},
|
|
31
|
+
{ enabled: !!batchOrder.id },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (page === 1) {
|
|
36
|
+
setAllSeats(seats);
|
|
37
|
+
} else if (seats.length > 0) {
|
|
38
|
+
setAllSeats((prev) => [...prev, ...seats]);
|
|
39
|
+
}
|
|
40
|
+
}, [seats]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setPage(1);
|
|
44
|
+
}, [query]);
|
|
45
|
+
|
|
46
|
+
const totalCount = meta?.pagination?.count ?? 0;
|
|
47
|
+
const remainingCount = Math.min(ITEMS_PER_PAGE, totalCount - allSeats.length);
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
!batchOrder.nb_seats ||
|
|
51
|
+
batchOrder.seats_owned === undefined ||
|
|
52
|
+
batchOrder.seats_to_own === undefined
|
|
53
|
+
) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="dashboard__quote__enrollment">
|
|
59
|
+
<div className="content">
|
|
60
|
+
<div className="enrollment-progress">
|
|
61
|
+
<span className="dashboard-item__label">
|
|
62
|
+
{intl.formatMessage(batchOrderSeatInfoMessages.enrolledParticipants, {
|
|
63
|
+
seats_owned: seatsOwnedCount,
|
|
64
|
+
nb_seats: batchOrder.nb_seats,
|
|
65
|
+
})}
|
|
66
|
+
</span>
|
|
67
|
+
<div className="enrollment-progress__bar">
|
|
68
|
+
<div
|
|
69
|
+
className="enrollment-progress__bar__fill"
|
|
70
|
+
style={{ width: `${(seatsOwnedCount / batchOrder.nb_seats) * 100}%` }}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
{states.error && <Banner message={states.error} type={BannerType.ERROR} />}
|
|
75
|
+
<div className="enrollment-nested-section__content">
|
|
76
|
+
<Input
|
|
77
|
+
className="enrollment-search"
|
|
78
|
+
label={intl.formatMessage(batchOrderSeatInfoMessages.searchPlaceholder)}
|
|
79
|
+
value={query}
|
|
80
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
81
|
+
rightIcon={<Icon name={IconTypeEnum.MAGNIFYING_GLASS} size="small" />}
|
|
82
|
+
/>
|
|
83
|
+
{allSeats.length === 0 && query ? (
|
|
84
|
+
<FormattedMessage {...batchOrderSeatInfoMessages.noResults} />
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
<ul className="enrollment-list">
|
|
88
|
+
{allSeats.map((seat) => (
|
|
89
|
+
<li key={seat.id}>{seat.owner_name ?? seat.voucher}</li>
|
|
90
|
+
))}
|
|
91
|
+
</ul>
|
|
92
|
+
{remainingCount > 0 && (
|
|
93
|
+
<Button
|
|
94
|
+
className="enrollment-load-more"
|
|
95
|
+
color="brand"
|
|
96
|
+
variant="secondary"
|
|
97
|
+
size="small"
|
|
98
|
+
onClick={() => setPage((p) => p + 1)}
|
|
99
|
+
disabled={states.fetching}
|
|
100
|
+
>
|
|
101
|
+
{intl.formatMessage(batchOrderSeatInfoMessages.loadMore, {
|
|
102
|
+
count: remainingCount,
|
|
103
|
+
})}
|
|
104
|
+
</Button>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -38,3 +38,20 @@
|
|
|
38
38
|
display: flex;
|
|
39
39
|
justify-content: center;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
.dashboard__quote__enrollment {
|
|
43
|
+
margin-top: 1rem;
|
|
44
|
+
padding-top: 1rem;
|
|
45
|
+
border-top: 1px solid r-theme-val(dashboard-card, base-color);
|
|
46
|
+
|
|
47
|
+
&__title {
|
|
48
|
+
margin: 0 0 0.5rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.content {
|
|
52
|
+
font-size: 0.8rem;
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
gap: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -50,7 +50,7 @@ describe('full process for the organization quotes dashboard', () => {
|
|
|
50
50
|
batch_order: {
|
|
51
51
|
state: BatchOrderState.QUOTED,
|
|
52
52
|
payment_method: PaymentMethod.CARD_PAYMENT,
|
|
53
|
-
available_actions: { next_action: 'confirm_quote' },
|
|
53
|
+
available_actions: { next_action: 'confirm_quote', download_quote: true },
|
|
54
54
|
},
|
|
55
55
|
organization_signed_on: undefined,
|
|
56
56
|
}).one();
|
|
@@ -214,7 +214,6 @@ describe('full process for the organization quotes dashboard', () => {
|
|
|
214
214
|
},
|
|
215
215
|
}).one();
|
|
216
216
|
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', organization);
|
|
217
|
-
|
|
218
217
|
const quoteQuoted = OrganizationQuoteFactory({
|
|
219
218
|
batch_order: {
|
|
220
219
|
state: BatchOrderState.QUOTED,
|
|
@@ -272,6 +271,10 @@ describe('full process for the organization quotes dashboard', () => {
|
|
|
272
271
|
`https://joanie.endpoint/api/v1.0/organizations/1/submit-for-signature-batch-order/`,
|
|
273
272
|
200,
|
|
274
273
|
);
|
|
274
|
+
fetchMock.get(
|
|
275
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${quoteCompleted.batch_order.id}/seats/?page=1&page_size=10`,
|
|
276
|
+
{ results: [], count: 0, previous: null, next: null },
|
|
277
|
+
);
|
|
275
278
|
|
|
276
279
|
render(<TeacherDashboardOrganizationQuotes />, {
|
|
277
280
|
routerOptions: {
|
|
@@ -28,7 +28,9 @@ describe('pages/TeacherDashboardOrganizationQuotes', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('should render a list of quotes for an organization', async () => {
|
|
31
|
-
const quoteList = OrganizationQuoteFactory(
|
|
31
|
+
const quoteList = OrganizationQuoteFactory({
|
|
32
|
+
batch_order: { state: BatchOrderState.QUOTED },
|
|
33
|
+
}).many(1);
|
|
32
34
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
|
|
33
35
|
|
|
34
36
|
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', []);
|
|
@@ -85,7 +87,9 @@ describe('pages/TeacherDashboardOrganizationQuotes', () => {
|
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
it('should paginate', async () => {
|
|
88
|
-
const quoteList = OrganizationQuoteFactory(
|
|
90
|
+
const quoteList = OrganizationQuoteFactory({
|
|
91
|
+
batch_order: { state: BatchOrderState.QUOTED },
|
|
92
|
+
}).many(30);
|
|
89
93
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
|
|
90
94
|
|
|
91
95
|
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', []);
|
|
@@ -158,7 +162,7 @@ describe('pages/TeacherDashboardOrganizationQuotes', () => {
|
|
|
158
162
|
const quoteQuoted = OrganizationQuoteFactory({
|
|
159
163
|
batch_order: {
|
|
160
164
|
state: BatchOrderState.QUOTED,
|
|
161
|
-
available_actions: { next_action: 'confirm_quote' },
|
|
165
|
+
available_actions: { next_action: 'confirm_quote', download_quote: true },
|
|
162
166
|
},
|
|
163
167
|
}).one();
|
|
164
168
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
|
|
@@ -14,6 +14,7 @@ import Badge from 'components/Badge';
|
|
|
14
14
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
15
15
|
import { browserDownloadFromBlob } from 'utils/download';
|
|
16
16
|
import { Spinner } from 'components/Spinner';
|
|
17
|
+
import { BatchOrderSeatInfoQuote } from './BatchOrderSeatInfoQuote';
|
|
17
18
|
|
|
18
19
|
const messages = defineMessages({
|
|
19
20
|
loading: {
|
|
@@ -360,35 +361,40 @@ const TeacherDashboardOrganizationQuotes = () => {
|
|
|
360
361
|
);
|
|
361
362
|
};
|
|
362
363
|
|
|
364
|
+
const renderDownloadButton = (quote: OrganizationQuote) => {
|
|
365
|
+
const batchOrder = quote.batch_order;
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<Button
|
|
369
|
+
size="small"
|
|
370
|
+
color="brand"
|
|
371
|
+
variant="secondary"
|
|
372
|
+
className="mr-2"
|
|
373
|
+
onClick={() => handleDownloadQuote(quote.id)}
|
|
374
|
+
icon={<span className="material-icons">download</span>}
|
|
375
|
+
disabled={!abilities?.download_quote || !batchOrder.available_actions.download_quote}
|
|
376
|
+
>
|
|
377
|
+
{intl.formatMessage(messages.downloadQuote)}
|
|
378
|
+
</Button>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
363
382
|
const renderActionButton = (quote: OrganizationQuote) => {
|
|
364
383
|
const batchOrder = quote.batch_order;
|
|
365
384
|
const state = batchOrder?.state;
|
|
366
385
|
const paymentMethod = batchOrder?.payment_method;
|
|
367
386
|
|
|
368
|
-
if (!batchOrder || !state || !paymentMethod
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
<
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
disabled={!abilities?.download_quote}
|
|
380
|
-
>
|
|
381
|
-
{intl.formatMessage(messages.downloadQuote)}
|
|
382
|
-
</Button>
|
|
383
|
-
<Button
|
|
384
|
-
size="small"
|
|
385
|
-
onClick={() => handleOpenConfirm(quote.id)}
|
|
386
|
-
icon={<span className="material-icons">check_circle</span>}
|
|
387
|
-
disabled={!abilities?.confirm_quote}
|
|
388
|
-
>
|
|
389
|
-
{intl.formatMessage(messages.confirmQuote)}
|
|
390
|
-
</Button>
|
|
391
|
-
</div>
|
|
387
|
+
if (!batchOrder || !state || !paymentMethod) return null;
|
|
388
|
+
|
|
389
|
+
const confirmQuoteButton = (
|
|
390
|
+
<Button
|
|
391
|
+
size="small"
|
|
392
|
+
onClick={() => handleOpenConfirm(quote.id)}
|
|
393
|
+
icon={<span className="material-icons">check_circle</span>}
|
|
394
|
+
disabled={!abilities?.confirm_quote}
|
|
395
|
+
>
|
|
396
|
+
{intl.formatMessage(messages.confirmQuote)}
|
|
397
|
+
</Button>
|
|
392
398
|
);
|
|
393
399
|
|
|
394
400
|
const confirmPurchaseOrderButton = (
|
|
@@ -430,7 +436,7 @@ const TeacherDashboardOrganizationQuotes = () => {
|
|
|
430
436
|
|
|
431
437
|
switch (batchOrder.available_actions?.next_action) {
|
|
432
438
|
case 'confirm_quote':
|
|
433
|
-
return
|
|
439
|
+
return confirmQuoteButton;
|
|
434
440
|
case 'confirm_purchase_order':
|
|
435
441
|
return confirmPurchaseOrderButton;
|
|
436
442
|
case 'confirm_bank_transfer':
|
|
@@ -485,7 +491,10 @@ const TeacherDashboardOrganizationQuotes = () => {
|
|
|
485
491
|
</Badge>
|
|
486
492
|
)}
|
|
487
493
|
</div>
|
|
488
|
-
<div className="dashboard__quote__header__action">
|
|
494
|
+
<div className="dashboard__quote__header__action">
|
|
495
|
+
{renderDownloadButton(quote)}
|
|
496
|
+
{renderActionButton(quote)}
|
|
497
|
+
</div>
|
|
489
498
|
</div>
|
|
490
499
|
}
|
|
491
500
|
defaultExpanded={false}
|
|
@@ -534,6 +543,9 @@ const TeacherDashboardOrganizationQuotes = () => {
|
|
|
534
543
|
</div>
|
|
535
544
|
)}
|
|
536
545
|
</div>
|
|
546
|
+
{quote.batch_order.state === BatchOrderState.COMPLETED && (
|
|
547
|
+
<BatchOrderSeatInfoQuote batchOrder={quote.batch_order} />
|
|
548
|
+
)}
|
|
537
549
|
</DashboardCard>
|
|
538
550
|
))}
|
|
539
551
|
<Pagination {...pagination} />
|
package/js/types/Joanie.ts
CHANGED
|
@@ -567,6 +567,14 @@ export interface BatchOrderRead {
|
|
|
567
567
|
funding_entity?: string;
|
|
568
568
|
funding_amount?: number;
|
|
569
569
|
offering: OfferingBatchOrder;
|
|
570
|
+
seats_to_own?: number;
|
|
571
|
+
seats_owned?: number;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export interface BatchOrderSeat {
|
|
575
|
+
id: string;
|
|
576
|
+
owner_name: string | null;
|
|
577
|
+
voucher: string | null;
|
|
570
578
|
}
|
|
571
579
|
|
|
572
580
|
export interface Relation {
|
|
@@ -585,7 +593,8 @@ export type BatchOrderAction =
|
|
|
585
593
|
| 'confirm_quote'
|
|
586
594
|
| 'confirm_purchase_order'
|
|
587
595
|
| 'confirm_bank_transfer'
|
|
588
|
-
| 'submit_for_signature'
|
|
596
|
+
| 'submit_for_signature'
|
|
597
|
+
| 'download_quote';
|
|
589
598
|
|
|
590
599
|
export type BatchOrderAvailableActions = {
|
|
591
600
|
[K in BatchOrderAction]: boolean;
|
|
@@ -603,6 +612,8 @@ export interface BatchOrderQuote {
|
|
|
603
612
|
payment_method: PaymentMethod;
|
|
604
613
|
contract_submitted: boolean;
|
|
605
614
|
nb_seats: number;
|
|
615
|
+
seats_to_own?: number;
|
|
616
|
+
seats_owned?: number;
|
|
606
617
|
available_actions: BatchOrderAvailableActions;
|
|
607
618
|
}
|
|
608
619
|
|
|
@@ -741,6 +752,10 @@ export interface OfferingQueryFilters extends PaginatedResourceQuery {
|
|
|
741
752
|
export interface BatchOrderQueryFilters extends PaginatedResourceQuery {
|
|
742
753
|
id?: BatchOrderRead['id'];
|
|
743
754
|
}
|
|
755
|
+
export interface BatchOrderSeatsQueryFilters extends PaginatedResourceQuery {
|
|
756
|
+
batch_order_id?: string;
|
|
757
|
+
query?: string;
|
|
758
|
+
}
|
|
744
759
|
export interface OrganizationQuoteQueryFilters extends PaginatedResourceQuery {
|
|
745
760
|
organization_id?: Organization['id'];
|
|
746
761
|
batch_order_id?: BatchOrder['id'];
|
|
@@ -834,6 +849,10 @@ interface APIUser {
|
|
|
834
849
|
submit_for_payment: {
|
|
835
850
|
create(filters: ResourcesQuery): Promise<any>;
|
|
836
851
|
};
|
|
852
|
+
seats: {
|
|
853
|
+
get(filters?: BatchOrderSeatsQueryFilters): Promise<PaginatedResponse<BatchOrderSeat>>;
|
|
854
|
+
};
|
|
855
|
+
seats_export(id: string): Promise<File>;
|
|
837
856
|
};
|
|
838
857
|
certificates: {
|
|
839
858
|
download(id: string): Promise<File>;
|
|
@@ -947,6 +966,7 @@ export interface API {
|
|
|
947
966
|
): ContractResourceQuery extends { id: string }
|
|
948
967
|
? Promise<Nullable<Agreement>>
|
|
949
968
|
: Promise<PaginatedResponse<Agreement>>;
|
|
969
|
+
download(filters: { organization_id: string; id: string }): Promise<File>;
|
|
950
970
|
};
|
|
951
971
|
};
|
|
952
972
|
courseRuns: {
|
package/js/utils/download.ts
CHANGED
|
@@ -5,11 +5,13 @@ import { handle } from './errors/handle';
|
|
|
5
5
|
*
|
|
6
6
|
* @param downloadFunction, an api promise that return a File
|
|
7
7
|
* @param newWindow, does it open in a new window or not
|
|
8
|
+
* @param filename, optional filename override; if provided, takes precedence over file.name
|
|
8
9
|
* @returns boolean, true for success
|
|
9
10
|
*/
|
|
10
11
|
export const browserDownloadFromBlob = async (
|
|
11
12
|
downloadFunction: () => Promise<File>,
|
|
12
13
|
newWindow: boolean = false,
|
|
14
|
+
filename?: string,
|
|
13
15
|
) => {
|
|
14
16
|
try {
|
|
15
17
|
const file = await downloadFunction();
|
|
@@ -24,7 +26,7 @@ export const browserDownloadFromBlob = async (
|
|
|
24
26
|
|
|
25
27
|
const $link = document.createElement('a');
|
|
26
28
|
$link.href = url;
|
|
27
|
-
$link.download = file.name;
|
|
29
|
+
$link.download = filename || file.name;
|
|
28
30
|
|
|
29
31
|
const revokeObject = () => {
|
|
30
32
|
// eslint-disable-next-line compat/compat
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
BatchOrderQuote,
|
|
47
47
|
Relation,
|
|
48
48
|
Agreement,
|
|
49
|
+
BatchOrderSeat,
|
|
49
50
|
} from 'types/Joanie';
|
|
50
51
|
import { Payment, PaymentMethod, PaymentProviders } from 'components/PaymentInterfaces/types';
|
|
51
52
|
import { CourseStateFactory } from 'utils/test/factories/richie';
|
|
@@ -214,12 +215,15 @@ export const BatchOrderQuoteFactory = factory((): BatchOrderQuote => {
|
|
|
214
215
|
relation: RelationFactory().one(),
|
|
215
216
|
payment_method: faker.helpers.arrayElement(Object.values(PaymentMethod)),
|
|
216
217
|
contract_submitted: faker.datatype.boolean(),
|
|
217
|
-
nb_seats: faker.number.int({ min:
|
|
218
|
+
nb_seats: faker.number.int({ min: 10, max: 100 }),
|
|
219
|
+
seats_owned: faker.number.int({ min: 0, max: 10 }),
|
|
220
|
+
seats_to_own: faker.number.int({ min: 90, max: 100 }),
|
|
218
221
|
available_actions: {
|
|
219
222
|
confirm_quote: false,
|
|
220
223
|
confirm_purchase_order: false,
|
|
221
224
|
confirm_bank_transfer: false,
|
|
222
225
|
submit_for_signature: false,
|
|
226
|
+
download_quote: false,
|
|
223
227
|
next_action: null,
|
|
224
228
|
},
|
|
225
229
|
};
|
|
@@ -551,6 +555,8 @@ export const BatchOrderReadFactory = factory((): BatchOrderRead => {
|
|
|
551
555
|
funding_entity: faker.company.name(),
|
|
552
556
|
funding_amount: faker.number.int({ min: 100, max: 10000 }),
|
|
553
557
|
offering: OfferingBatchOrderFactory().one(),
|
|
558
|
+
seats_owned: faker.number.int({ min: 1, max: 200 }),
|
|
559
|
+
seats_to_own: faker.number.int({ min: 1, max: 200 }),
|
|
554
560
|
};
|
|
555
561
|
});
|
|
556
562
|
|
|
@@ -678,3 +684,11 @@ export const SaleTunnelContextFactory = factory(
|
|
|
678
684
|
setPaymentMode: noop,
|
|
679
685
|
}),
|
|
680
686
|
);
|
|
687
|
+
|
|
688
|
+
export const BatchOrderSeatFactory = factory((): BatchOrderSeat => {
|
|
689
|
+
return {
|
|
690
|
+
id: faker.string.uuid(),
|
|
691
|
+
owner_name: null,
|
|
692
|
+
voucher: faker.string.alphanumeric(10),
|
|
693
|
+
};
|
|
694
|
+
});
|