Path: blob/master/src/packages/frontend/course/configuration/student-pay.tsx
1503 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";1415import { Gap, Icon, TimeAgo } from "@cocalc/frontend/components";16import { labels } from "@cocalc/frontend/i18n";17import LicenseEditor from "@cocalc/frontend/purchases/license-editor";18import MoneyStatistic from "@cocalc/frontend/purchases/money-statistic";19import { webapp_client } from "@cocalc/frontend/webapp-client";20import { compute_cost } from "@cocalc/util/licenses/purchase/compute-cost";21import { DEFAULT_PURCHASE_INFO } from "@cocalc/util/licenses/purchase/student-pay";22import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";23import { currency } from "@cocalc/util/misc";2425export default function StudentPay({ actions, settings }) {26const intl = useIntl();2728const [minPayment, setMinPayment] = useState<number | undefined>(undefined);29const updateMinPayment = () => {30(async () => {31setMinPayment(await webapp_client.purchases_client.getMinimumPayment());32})();33};34useEffect(() => {35updateMinPayment();36}, []);3738const [info, setInfo] = useState<PurchaseInfo>(() => {39const cur = settings.get("payInfo")?.toJS();40if (cur != null) {41return cur;42}43const info = {44...DEFAULT_PURCHASE_INFO,45start: new Date(),46end: dayjs().add(3, "month").toDate(),47} as PurchaseInfo;48setTimeout(() => {49// React requirment: this must happen in different render loop, because50// it causes an update to the UI.51actions.configuration.setStudentPay({ info, cost });52}, 1);53return info;54});5556if (info.type == "vouchers") {57// for typescript58throw Error("bug");59}6061const getWhenFromSettings = () => {62const pay = settings.get("pay");63if (pay) {64return dayjs(pay);65}66if (info.start) {67return dayjs(info.start).add(7, "day");68}69return dayjs().add(7, "day");70};7172const [when, setWhen] = useState<dayjs.Dayjs>(getWhenFromSettings);73const cost = useMemo(() => {74try {75return compute_cost(info).cost;76} catch (_) {77return null;78}79}, [info]);8081const [showStudentPay, setShowStudentPay] = useState<boolean>(false);82const reset = () => {83const cur = settings.get("payInfo")?.toJS();84if (cur != null) {85setInfo(cur);86}87setWhen(getWhenFromSettings());88};8990useEffect(() => {91// whenever opening the panel to edit, set controls to what is in the store.92if (showStudentPay) {93reset();94}95}, [showStudentPay]);9697useEffect(() => {98// this makes it sync with any other editor when closed.99if (!showStudentPay) {100reset();101}102}, [settings.get("payInfo")]);103104const paySelected = useMemo(() => {105if (!settings) return false;106return settings.get("student_pay") || settings.get("institute_pay");107}, [settings]);108109if (settings == null || actions == null) {110return <Spin />;111}112113const buttons = showStudentPay ? (114<Space style={{ margin: "10px 0", float: "right" }}>115<Button116onClick={() => {117setShowStudentPay(false);118reset();119}}120>121{intl.formatMessage(labels.cancel)}122</Button>123<Button124disabled={125isEqual(info, settings.get("payInfo")?.toJS()) &&126when.isSame(dayjs(settings.get("pay")))127}128type="primary"129onClick={() => {130actions.configuration.setStudentPay({ info, when, cost });131}}132>133{intl.formatMessage(labels.save_changes)}134</Button>135</Space>136) : undefined;137138return (139<Card140style={!paySelected ? { background: "#fcf8e3" } : undefined}141title={142<>143<Icon name="dashboard" />{" "}144<FormattedMessage145id="course.student-pay.title"146defaultMessage={"Require Students to Upgrade (Students Pay)"}147/>148</>149}150>151{cost != null && !showStudentPay && !!settings?.get("student_pay") && (152<div style={{ float: "right" }}>153<MoneyStatistic title="Cost Per Student" value={cost} />154</div>155)}156<Checkbox157checked={!!settings?.get("student_pay")}158onChange={(e) => {159actions.configuration.set_pay_choice("student", e.target.checked);160if (e.target.checked) {161setShowStudentPay(true);162actions.configuration.setStudentPay({163when: getWhenFromSettings(),164info,165cost,166});167actions.configuration.configure_all_projects();168}169}}170>171<FormattedMessage172id="course.student-pay.checkbox.students-pay"173defaultMessage={"Students pay directly"}174/>175</Checkbox>176{settings?.get("student_pay") && (177<div>178{buttons}179<Space style={{ margin: "10px 0" }}>180<Button181disabled={showStudentPay}182onClick={() => {183setShowStudentPay(true);184}}185>186<Icon name="credit-card" /> Start and end dates and upgrades...187</Button>188</Space>189<div>190{showStudentPay && (191<Alert192style={{ margin: "15px 0" }}193message={194<>195<Icon name="credit-card" /> Require Students to Upgrade196their Project197</>198}199description={200<div>201The cost is determined by the course length and desired202upgrades, which you configure below:203<div204style={{205height: "65px",206textAlign: "center",207}}208>209{cost != null && (210<MoneyStatistic title="Cost" value={cost} />211)}212</div>213<Divider>Configuration</Divider>214<LicenseEditor215noCancel216cellStyle={{ padding: 0, margin: "-10px 0" }}217info={info}218onChange={setInfo}219hiddenFields={new Set(["quantity", "custom_member"])}220/>221<div style={{ margin: "15px 0" }}>222<StudentPayCheckboxLabel223settings={settings}224when={when}225/>226</div>227{!!settings.get("pay") && (228<RequireStudentsPayWhen229when={when}230setWhen={setWhen}231cost={cost}232minPayment={minPayment}233info={info}234/>235)}236{buttons}237</div>238}239/>240)}241<hr />242<div style={{ color: "#666" }}>243<StudentPayDesc244settings={settings}245when={when}246cost={cost}247minPayment={minPayment}248/>249</div>250</div>251</div>252)}253</Card>254);255}256257function StudentPayCheckboxLabel({ settings, when }) {258if (settings.get("pay")) {259if (webapp_client.server_time() >= settings.get("pay")) {260return <span>Require that students upgrade immediately:</span>;261} else {262return (263<span>264Require that students upgrade by <TimeAgo date={when} />:{" "}265</span>266);267}268} else {269return <span>Require that students upgrade...</span>;270}271}272273function RequireStudentsPayWhen({ when, setWhen, cost, minPayment, info }) {274const start = dayjs(info.start);275return (276<div style={{ marginBottom: "15px" }}>277<div style={{ textAlign: "center", marginBottom: "15px" }}>278<DatePicker279changeOnBlur280showToday281allowClear={false}282disabledDate={(current) =>283current < start.subtract(1, "day") ||284current >= start.add(21, "day")285}286defaultValue={when}287onChange={(date) => {288setWhen(date ?? dayjs());289}}290/>291</div>292<RequireStudentPayDesc cost={cost} when={when} minPayment={minPayment} />293</div>294);295}296297function StudentPayDesc({ settings, cost, when, minPayment }) {298if (settings.get("pay")) {299return (300<span>301<span style={{ fontSize: "18pt" }}>302<Icon name="check" />303</span>{" "}304<Gap />305<RequireStudentPayDesc306cost={cost}307when={when}308minPayment={minPayment}309/>310</span>311);312} else {313return (314<span>315Require that all students in the course pay a one-time fee to upgrade316their project. This is strongly recommended, and ensures that your317students have a much better experience, and do not see a large{" "}318<span319style={{ color: "white", background: "darkred", padding: "0 5px" }}320>321RED warning banner322</span>{" "}323all the time. Alternatively, you (or your university) can pay for all324students -- see below.325</span>326);327}328}329330function RequireStudentPayDesc({ cost, when, minPayment }) {331if (when > dayjs()) {332return (333<span>334<b>335Your students will see a warning until <TimeAgo date={when} />.336</b>{" "}337{cost != null && (338<>339They will then be required to upgrade for a{" "}340<b>one-time fee of {currency(cost)}</b>. This cost in USD is locked341in, even if the rates on our site change.{" "}342{minPayment != null && cost < minPayment343? `NOTE: Students will have344to pay ${currency(345minPayment,346)} since that is the minimum transaction; they can use excess credit for other purchases.`347: ""}348</>349)}350</span>351);352} else {353return (354<span>355<b>356Your students are required to upgrade their project now to use it.357</b>{" "}358If you want to give them more time to upgrade, move the date forward.359</span>360);361}362}363364365