Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/configuration/student-pay.tsx
1503 views
1
import {
2
Alert,
3
Button,
4
Card,
5
Checkbox,
6
DatePicker,
7
Divider,
8
Space,
9
Spin,
10
} from "antd";
11
import dayjs from "dayjs";
12
import { isEqual } from "lodash";
13
import { useEffect, useMemo, useState } from "react";
14
import { FormattedMessage, useIntl } from "react-intl";
15
16
import { Gap, Icon, TimeAgo } from "@cocalc/frontend/components";
17
import { labels } from "@cocalc/frontend/i18n";
18
import LicenseEditor from "@cocalc/frontend/purchases/license-editor";
19
import MoneyStatistic from "@cocalc/frontend/purchases/money-statistic";
20
import { webapp_client } from "@cocalc/frontend/webapp-client";
21
import { compute_cost } from "@cocalc/util/licenses/purchase/compute-cost";
22
import { DEFAULT_PURCHASE_INFO } from "@cocalc/util/licenses/purchase/student-pay";
23
import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
24
import { currency } from "@cocalc/util/misc";
25
26
export default function StudentPay({ actions, settings }) {
27
const intl = useIntl();
28
29
const [minPayment, setMinPayment] = useState<number | undefined>(undefined);
30
const updateMinPayment = () => {
31
(async () => {
32
setMinPayment(await webapp_client.purchases_client.getMinimumPayment());
33
})();
34
};
35
useEffect(() => {
36
updateMinPayment();
37
}, []);
38
39
const [info, setInfo] = useState<PurchaseInfo>(() => {
40
const cur = settings.get("payInfo")?.toJS();
41
if (cur != null) {
42
return cur;
43
}
44
const info = {
45
...DEFAULT_PURCHASE_INFO,
46
start: new Date(),
47
end: dayjs().add(3, "month").toDate(),
48
} as PurchaseInfo;
49
setTimeout(() => {
50
// React requirment: this must happen in different render loop, because
51
// it causes an update to the UI.
52
actions.configuration.setStudentPay({ info, cost });
53
}, 1);
54
return info;
55
});
56
57
if (info.type == "vouchers") {
58
// for typescript
59
throw Error("bug");
60
}
61
62
const getWhenFromSettings = () => {
63
const pay = settings.get("pay");
64
if (pay) {
65
return dayjs(pay);
66
}
67
if (info.start) {
68
return dayjs(info.start).add(7, "day");
69
}
70
return dayjs().add(7, "day");
71
};
72
73
const [when, setWhen] = useState<dayjs.Dayjs>(getWhenFromSettings);
74
const cost = useMemo(() => {
75
try {
76
return compute_cost(info).cost;
77
} catch (_) {
78
return null;
79
}
80
}, [info]);
81
82
const [showStudentPay, setShowStudentPay] = useState<boolean>(false);
83
const reset = () => {
84
const cur = settings.get("payInfo")?.toJS();
85
if (cur != null) {
86
setInfo(cur);
87
}
88
setWhen(getWhenFromSettings());
89
};
90
91
useEffect(() => {
92
// whenever opening the panel to edit, set controls to what is in the store.
93
if (showStudentPay) {
94
reset();
95
}
96
}, [showStudentPay]);
97
98
useEffect(() => {
99
// this makes it sync with any other editor when closed.
100
if (!showStudentPay) {
101
reset();
102
}
103
}, [settings.get("payInfo")]);
104
105
const paySelected = useMemo(() => {
106
if (!settings) return false;
107
return settings.get("student_pay") || settings.get("institute_pay");
108
}, [settings]);
109
110
if (settings == null || actions == null) {
111
return <Spin />;
112
}
113
114
const buttons = showStudentPay ? (
115
<Space style={{ margin: "10px 0", float: "right" }}>
116
<Button
117
onClick={() => {
118
setShowStudentPay(false);
119
reset();
120
}}
121
>
122
{intl.formatMessage(labels.cancel)}
123
</Button>
124
<Button
125
disabled={
126
isEqual(info, settings.get("payInfo")?.toJS()) &&
127
when.isSame(dayjs(settings.get("pay")))
128
}
129
type="primary"
130
onClick={() => {
131
actions.configuration.setStudentPay({ info, when, cost });
132
}}
133
>
134
{intl.formatMessage(labels.save_changes)}
135
</Button>
136
</Space>
137
) : undefined;
138
139
return (
140
<Card
141
style={!paySelected ? { background: "#fcf8e3" } : undefined}
142
title={
143
<>
144
<Icon name="dashboard" />{" "}
145
<FormattedMessage
146
id="course.student-pay.title"
147
defaultMessage={"Require Students to Upgrade (Students Pay)"}
148
/>
149
</>
150
}
151
>
152
{cost != null && !showStudentPay && !!settings?.get("student_pay") && (
153
<div style={{ float: "right" }}>
154
<MoneyStatistic title="Cost Per Student" value={cost} />
155
</div>
156
)}
157
<Checkbox
158
checked={!!settings?.get("student_pay")}
159
onChange={(e) => {
160
actions.configuration.set_pay_choice("student", e.target.checked);
161
if (e.target.checked) {
162
setShowStudentPay(true);
163
actions.configuration.setStudentPay({
164
when: getWhenFromSettings(),
165
info,
166
cost,
167
});
168
actions.configuration.configure_all_projects();
169
}
170
}}
171
>
172
<FormattedMessage
173
id="course.student-pay.checkbox.students-pay"
174
defaultMessage={"Students pay directly"}
175
/>
176
</Checkbox>
177
{settings?.get("student_pay") && (
178
<div>
179
{buttons}
180
<Space style={{ margin: "10px 0" }}>
181
<Button
182
disabled={showStudentPay}
183
onClick={() => {
184
setShowStudentPay(true);
185
}}
186
>
187
<Icon name="credit-card" /> Start and end dates and upgrades...
188
</Button>
189
</Space>
190
<div>
191
{showStudentPay && (
192
<Alert
193
style={{ margin: "15px 0" }}
194
message={
195
<>
196
<Icon name="credit-card" /> Require Students to Upgrade
197
their Project
198
</>
199
}
200
description={
201
<div>
202
The cost is determined by the course length and desired
203
upgrades, which you configure below:
204
<div
205
style={{
206
height: "65px",
207
textAlign: "center",
208
}}
209
>
210
{cost != null && (
211
<MoneyStatistic title="Cost" value={cost} />
212
)}
213
</div>
214
<Divider>Configuration</Divider>
215
<LicenseEditor
216
noCancel
217
cellStyle={{ padding: 0, margin: "-10px 0" }}
218
info={info}
219
onChange={setInfo}
220
hiddenFields={new Set(["quantity", "custom_member"])}
221
/>
222
<div style={{ margin: "15px 0" }}>
223
<StudentPayCheckboxLabel
224
settings={settings}
225
when={when}
226
/>
227
</div>
228
{!!settings.get("pay") && (
229
<RequireStudentsPayWhen
230
when={when}
231
setWhen={setWhen}
232
cost={cost}
233
minPayment={minPayment}
234
info={info}
235
/>
236
)}
237
{buttons}
238
</div>
239
}
240
/>
241
)}
242
<hr />
243
<div style={{ color: "#666" }}>
244
<StudentPayDesc
245
settings={settings}
246
when={when}
247
cost={cost}
248
minPayment={minPayment}
249
/>
250
</div>
251
</div>
252
</div>
253
)}
254
</Card>
255
);
256
}
257
258
function StudentPayCheckboxLabel({ settings, when }) {
259
if (settings.get("pay")) {
260
if (webapp_client.server_time() >= settings.get("pay")) {
261
return <span>Require that students upgrade immediately:</span>;
262
} else {
263
return (
264
<span>
265
Require that students upgrade by <TimeAgo date={when} />:{" "}
266
</span>
267
);
268
}
269
} else {
270
return <span>Require that students upgrade...</span>;
271
}
272
}
273
274
function RequireStudentsPayWhen({ when, setWhen, cost, minPayment, info }) {
275
const start = dayjs(info.start);
276
return (
277
<div style={{ marginBottom: "15px" }}>
278
<div style={{ textAlign: "center", marginBottom: "15px" }}>
279
<DatePicker
280
changeOnBlur
281
showToday
282
allowClear={false}
283
disabledDate={(current) =>
284
current < start.subtract(1, "day") ||
285
current >= start.add(21, "day")
286
}
287
defaultValue={when}
288
onChange={(date) => {
289
setWhen(date ?? dayjs());
290
}}
291
/>
292
</div>
293
<RequireStudentPayDesc cost={cost} when={when} minPayment={minPayment} />
294
</div>
295
);
296
}
297
298
function StudentPayDesc({ settings, cost, when, minPayment }) {
299
if (settings.get("pay")) {
300
return (
301
<span>
302
<span style={{ fontSize: "18pt" }}>
303
<Icon name="check" />
304
</span>{" "}
305
<Gap />
306
<RequireStudentPayDesc
307
cost={cost}
308
when={when}
309
minPayment={minPayment}
310
/>
311
</span>
312
);
313
} else {
314
return (
315
<span>
316
Require that all students in the course pay a one-time fee to upgrade
317
their project. This is strongly recommended, and ensures that your
318
students have a much better experience, and do not see a large{" "}
319
<span
320
style={{ color: "white", background: "darkred", padding: "0 5px" }}
321
>
322
RED warning banner
323
</span>{" "}
324
all the time. Alternatively, you (or your university) can pay for all
325
students -- see below.
326
</span>
327
);
328
}
329
}
330
331
function RequireStudentPayDesc({ cost, when, minPayment }) {
332
if (when > dayjs()) {
333
return (
334
<span>
335
<b>
336
Your students will see a warning until <TimeAgo date={when} />.
337
</b>{" "}
338
{cost != null && (
339
<>
340
They will then be required to upgrade for a{" "}
341
<b>one-time fee of {currency(cost)}</b>. This cost in USD is locked
342
in, even if the rates on our site change.{" "}
343
{minPayment != null && cost < minPayment
344
? `NOTE: Students will have
345
to pay ${currency(
346
minPayment,
347
)} since that is the minimum transaction; they can use excess credit for other purchases.`
348
: ""}
349
</>
350
)}
351
</span>
352
);
353
} else {
354
return (
355
<span>
356
<b>
357
Your students are required to upgrade their project now to use it.
358
</b>{" "}
359
If you want to give them more time to upgrade, move the date forward.
360
</span>
361
);
362
}
363
}
364
365