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