Skip to content

Commit 8dccbed

Browse files
committed
✨(frontend) add enrollement to organization dashboard
wip commit will be edited
1 parent b25acca commit 8dccbed

File tree

8 files changed

+185
-5
lines changed

8 files changed

+185
-5
lines changed

src/frontend/js/pages/DashboardBatchOrderLayout/index.spec.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ describe('<DashboardBatchOrderLayout />', () => {
5151
const batchOrder = BatchOrderReadFactory().one();
5252
const seats = BatchOrderSeatFactory().many(2);
5353
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`, batchOrder);
54-
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/`, seats);
54+
fetchMock.get(
55+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/?page=1&page_size=10`,
56+
{
57+
results: seats,
58+
count: seats.length,
59+
previous: null,
60+
next: null,
61+
},
62+
);
5563

5664
render(
5765
WrapperWithDashboard(
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { useEffect, useState } from 'react';
2+
import { defineMessages, 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+
9+
const messages = defineMessages({
10+
enrollmentManagement: {
11+
id: 'batchOrder.enrollmentManagement.title',
12+
description: 'Title for enrollment management section',
13+
defaultMessage: 'Enrollment',
14+
},
15+
enrolledParticipants: {
16+
id: 'batchOrder.enrollmentManagement.enrolledParticipants',
17+
description: 'Progress label showing enrolled participants out of total seats',
18+
defaultMessage: '{seats_owned}/{nb_seats} enrolled participants',
19+
},
20+
searchPlaceholder: {
21+
id: 'batchOrder.enrollmentManagement.searchPlaceholder',
22+
description: 'Placeholder for the seat search input (student name or voucher)',
23+
defaultMessage: 'Student name',
24+
},
25+
noResults: {
26+
id: 'batchOrder.enrollmentManagement.noResults',
27+
description: 'Message shown when the student search returns no results',
28+
defaultMessage: 'No student matches your search.',
29+
},
30+
loadMore: {
31+
id: 'batchOrder.enrollmentManagement.loadMore',
32+
description: 'Button to load more seats',
33+
defaultMessage: 'Load {count} more',
34+
},
35+
});
36+
37+
const ITEMS_PER_PAGE = 10;
38+
39+
export const BatchOrderSeatInfoQuote = ({ batchOrder }: { batchOrder: BatchOrderQuote }) => {
40+
const intl = useIntl();
41+
const [query, setQuery] = useState('');
42+
const [page, setPage] = useState(1);
43+
const [allSeats, setAllSeats] = useState<BatchOrderSeat[]>([]);
44+
45+
const seatsOwnedCount = batchOrder.seats_owned ?? 0;
46+
47+
const {
48+
items: seats,
49+
meta,
50+
states,
51+
} = useBatchOrderSeats(
52+
{
53+
batch_order_id: batchOrder.id,
54+
query: query || undefined,
55+
page,
56+
page_size: ITEMS_PER_PAGE,
57+
},
58+
{ enabled: !!batchOrder.id },
59+
);
60+
61+
useEffect(() => {
62+
if (page === 1) {
63+
setAllSeats(seats);
64+
} else if (seats.length > 0) {
65+
setAllSeats((prev) => [...prev, ...seats]);
66+
}
67+
}, [seats]);
68+
69+
useEffect(() => {
70+
setPage(1);
71+
}, [query]);
72+
73+
const totalCount = meta?.pagination?.count ?? 0;
74+
const remainingCount = Math.min(ITEMS_PER_PAGE, totalCount - allSeats.length);
75+
76+
if (
77+
!batchOrder.nb_seats ||
78+
batchOrder.seats_owned === undefined ||
79+
batchOrder.seats_to_own === undefined
80+
) {
81+
return null;
82+
}
83+
84+
return (
85+
<div className="dashboard__quote__enrollment">
86+
<h6 className="dashboard__quote__enrollment__title">
87+
{intl.formatMessage(messages.enrollmentManagement)}
88+
</h6>
89+
<div className="content">
90+
<div className="enrollment-progress">
91+
<span className="dashboard-item__label">
92+
{intl.formatMessage(messages.enrolledParticipants, {
93+
seats_owned: seatsOwnedCount,
94+
nb_seats: batchOrder.nb_seats,
95+
})}
96+
</span>
97+
<div className="enrollment-progress__bar">
98+
<div
99+
className="enrollment-progress__bar__fill"
100+
style={{ width: `${(seatsOwnedCount / batchOrder.nb_seats) * 100}%` }}
101+
/>
102+
</div>
103+
</div>
104+
{states.error && <Banner message={states.error} type={BannerType.ERROR} />}
105+
<div className="enrollment-nested-section__content">
106+
<Input
107+
className="enrollment-search"
108+
label={intl.formatMessage(messages.searchPlaceholder)}
109+
value={query}
110+
onChange={(e) => setQuery(e.target.value)}
111+
rightIcon={<Icon name={IconTypeEnum.MAGNIFYING_GLASS} size="small" />}
112+
/>
113+
{allSeats.length === 0 && query ? (
114+
<FormattedMessage {...messages.noResults} />
115+
) : (
116+
<>
117+
<ul className="enrollment-list">
118+
{allSeats.map((seat) => (
119+
<li key={seat.id}>{seat.owner_name ?? seat.voucher}</li>
120+
))}
121+
</ul>
122+
{remainingCount > 0 && (
123+
<Button
124+
className="enrollment-load-more"
125+
color="neutral"
126+
variant="secondary"
127+
size="small"
128+
onClick={() => setPage((p) => p + 1)}
129+
disabled={states.fetching}
130+
>
131+
{intl.formatMessage(messages.loadMore, { count: remainingCount })}
132+
</Button>
133+
)}
134+
</>
135+
)}
136+
</div>
137+
</div>
138+
</div>
139+
);
140+
};

src/frontend/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,20 @@
3838
display: flex;
3939
justify-content: center;
4040
}
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+
}

src/frontend/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ describe('full process for the organization quotes dashboard', () => {
214214
},
215215
}).one();
216216
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', organization);
217-
218217
const quoteQuoted = OrganizationQuoteFactory({
219218
batch_order: {
220219
state: BatchOrderState.QUOTED,
@@ -272,6 +271,10 @@ describe('full process for the organization quotes dashboard', () => {
272271
`https://joanie.endpoint/api/v1.0/organizations/1/submit-for-signature-batch-order/`,
273272
200,
274273
);
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+
);
275278

276279
render(<TeacherDashboardOrganizationQuotes />, {
277280
routerOptions: {

src/frontend/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ describe('pages/TeacherDashboardOrganizationQuotes', () => {
2828
});
2929

3030
it('should render a list of quotes for an organization', async () => {
31-
const quoteList = OrganizationQuoteFactory().many(1);
31+
const quoteList = OrganizationQuoteFactory({
32+
batch_order: { state: BatchOrderState.QUOTED },
33+
}).many(1);
3234
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
3335

3436
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', []);
@@ -85,7 +87,9 @@ describe('pages/TeacherDashboardOrganizationQuotes', () => {
8587
});
8688

8789
it('should paginate', async () => {
88-
const quoteList = OrganizationQuoteFactory().many(30);
90+
const quoteList = OrganizationQuoteFactory({
91+
batch_order: { state: BatchOrderState.QUOTED },
92+
}).many(30);
8993
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
9094

9195
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', []);

src/frontend/js/pages/TeacherDashboardOrganizationQuotes/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Badge from 'components/Badge';
1414
import { Icon, IconTypeEnum } from 'components/Icon';
1515
import { browserDownloadFromBlob } from 'utils/download';
1616
import { Spinner } from 'components/Spinner';
17+
import { BatchOrderSeatInfoQuote } from './BatchOrderSeatInfoQuote';
1718

1819
const messages = defineMessages({
1920
loading: {
@@ -534,6 +535,9 @@ const TeacherDashboardOrganizationQuotes = () => {
534535
</div>
535536
)}
536537
</div>
538+
{quote.batch_order.state === BatchOrderState.COMPLETED && (
539+
<BatchOrderSeatInfoQuote batchOrder={quote.batch_order} />
540+
)}
537541
</DashboardCard>
538542
))}
539543
<Pagination {...pagination} />

src/frontend/js/types/Joanie.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,8 @@ export interface BatchOrderQuote {
611611
payment_method: PaymentMethod;
612612
contract_submitted: boolean;
613613
nb_seats: number;
614+
seats_to_own?: number;
615+
seats_owned?: number;
614616
available_actions: BatchOrderAvailableActions;
615617
}
616618

src/frontend/js/utils/test/factories/joanie.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,9 @@ export const BatchOrderQuoteFactory = factory((): BatchOrderQuote => {
215215
relation: RelationFactory().one(),
216216
payment_method: faker.helpers.arrayElement(Object.values(PaymentMethod)),
217217
contract_submitted: faker.datatype.boolean(),
218-
nb_seats: faker.number.int({ min: 1, max: 100 }),
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 }),
219221
available_actions: {
220222
confirm_quote: false,
221223
confirm_purchase_order: false,

0 commit comments

Comments
 (0)