CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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