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