master
alex 2024-03-30 20:48:49 +01:00
parent f646a19cdf
commit 6694a48b44
4 changed files with 275 additions and 127 deletions

View File

@ -20,6 +20,7 @@
"hour": "Stunde",
"minutes": "Minuten",
"minute": "Minute",
"daysLong": "Tagen",
"days": "Tage",
"day": "Tag",
"separator": "und"
@ -507,13 +508,38 @@
]
},
"paymentPlan": {
"title": "Zahlungsplan",
"buttonUpdateBillingDetails": "Zahlungsdetails aktualisieren",
"plan": "Plan",
"contentsOfSubscription": {
"title": "Inhalt des Abonnements",
"maxEmployees": "Maximale Anzahl von Mitarbeitern",
"calendarMaxFutureBookingDays": "Maximale Anzahl von Tagen im Voraus"
}
"title": "Features & Preisübersicht",
"buttonMonthly": "Monatlich",
"buttonYearly": "Jährlich",
"buttonManagePlan": "Zahlungsplan verwalten",
"buttonBuyPlan": "Jetzt buchen",
"monthly": "monatlich",
"businessPlan": "Unternehmensplan",
"cancelledOn": "Gekündigt am {{date}} Uhr",
"changeToAnnualPayment": "Wechseln Sie zur jährlichen Zahlung und ",
"saveAmount": "sparen Sie {{amount}} €",
"popconfirmChangePlan": {
"title": "Zahlungsplan wechseln",
"description": "Sind Sie sicher, dass Sie Ihren Plan ändern wollen?"
},
"alertDemoEndsIn": {
"demoEndsIn": "Die Testversion endet in",
"selectPlanToAccessFullFeatures": "Wählen Sie einen Plan, um auf alle Funktionen zuzugreifen."
},
"alertPlanCancelled": "Ihr Plan wurde gekündigt. Am Ende des Abrechnungszeitraums müssen Sie einen neuen Plan auswählen, um unsere Dienste weiterhin nutzen zu können.",
"features": [
{
"text": "Unbegrenzte <b>{{bold}}</b>",
"bold": "Terminanzahl"
},
{
"text": "Bis zu <b>{{bold}}</b>",
"bold": "20 Mitarbeiter"
},
{
"text": "Buchungen bis zu <b>{{bold}}</b> im Voraus",
"bold": "7 Wochen"
}
]
}
}

View File

@ -20,6 +20,7 @@
"hour": "hour",
"minutes": "minutes",
"minute": "minute",
"daysLong": "days",
"days": "days",
"day": "day",
"separator": "and"
@ -516,13 +517,38 @@
]
},
"paymentPlan": {
"title": "Payment plan",
"buttonUpdateBillingDetails": "Update billing details",
"plan": "Plan",
"contentsOfSubscription": {
"title": "Contents of subscription",
"maxEmployees": "Max. employees",
"calendarMaxFutureBookingDays": "Max. future booking days"
}
"title": "Features & Pricing",
"buttonMonthly": "Monthly",
"buttonYearly": "Yearly",
"buttonManagePlan": "Manage plan",
"buttonBuyPlan": "Buy now",
"monthly": "monthly",
"businessPlan": "Business Plan",
"cancelledOn": "Cancelled on {{date}} o'clock",
"changeToAnnualPayment": "Switch to annual payment and ",
"saveAmount": "save {{amount}} €",
"popconfirmChangePlan": {
"title": "Change plan",
"description": "Are you sure you want to change your plan?"
},
"alertDemoEndsIn": {
"demoEndsIn": "Demo ends in",
"selectPlanToAccessFullFeatures": "Select a plan to access all features."
},
"alertPlanCancelled": "Your plan has been cancelled. At the end of the billing period, you need to select a new plan to continue using our services.",
"features": [
{
"text": "Unlimited <b>{{bold}}</b>",
"bold": "number of appointments"
},
{
"text": "Up to <b>{{bold}}</b>",
"bold": "20 employees"
},
{
"text": "Bookings up to <b>{{bold}}</b> in advance",
"bold": "7 weeks"
}
]
}
}

View File

@ -174,17 +174,10 @@ export function SideMenuContent({
}
}, [location.pathname]);
const calculateExpiry = () => {
const currentDate = new Date();
const expiryDate = new Date(sideBarContext.paymentPlanTrialEnd);
const diffTime = Math.abs(expiryDate - currentDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const accountPlanExpiry = calculateExpiry();
const daysLeft = Math.floor(
(new Date(sideBarContext.paymentPlanTrialEnd) - new Date()) /
(1000 * 60 * 60 * 24)
);
return (
<div
@ -254,9 +247,9 @@ export function SideMenuContent({
style={{ color: "#fff", textAlign: "center" }}
>
{t("sideMenu.paymentPlanTrailingDaysLeft", {
daysLeft: accountPlanExpiry,
daysLeft: daysLeft,
dayUnit:
accountPlanExpiry > 1
daysLeft > 1
? t("common.unit.days")
: t("common.unit.day"),
})}

View File

@ -10,25 +10,26 @@ import {
Grid,
Skeleton,
notification,
Popconfirm,
Tag,
} from "antd";
import { useAppContext } from "../../Contexts/AppContext";
import {
AppStyle,
FormatDatetime,
myFetch,
showUnkownErrorNotification,
} from "../../utils";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { CheckOutlined, CloseOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { ReactComponent as RocketLaunch } from "./rocket_launch_FILL1_wght400_GRAD0_opsz24.svg";
import { ReactComponent as Check } from "./task_alt_FILL0_wght400_GRAD0_opsz24.svg";
import CountUp from "react-countup";
const { useBreakpoint } = Grid;
export default function PaymentPlan() {
const { t } = useTranslation();
const appContext = useAppContext();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
@ -62,8 +63,7 @@ export default function PaymentPlan() {
setSelectedBillingPeriod(data.payment_plan_interval);
}
})
.catch((error) => {
console.log(error);
.catch(() => {
setIsRequesting(false);
showUnkownErrorNotification(notificationApi, t);
});
@ -71,19 +71,31 @@ export default function PaymentPlan() {
useEffect(() => fetchPaymentPlan(), []);
const showAlert = () => {
const showAlertTrialEnds = () => {
if (
!isRequesting &&
requestData.payment_plan === 0 &&
requestData.payment_plan_trial_end &&
!requestData.payment_plan_canceled_at
) {
const daysLeft = Math.floor(
(new Date(requestData.payment_plan_trial_end) - new Date()) /
(1000 * 60 * 60 * 24)
);
return (
<Alert
message={
<Typography.Text>
Ihre Demo endet in <b>7 Tagen</b>. Jetzt einen Plan auswählen, um
den vollen Funktionsumfang zu nutzen.
{t("paymentPlan.alertDemoEndsIn.demoEndsIn")}{" "}
<b>
{daysLeft}{" "}
{daysLeft === 1
? t("common.unit.day")
: t("common.unit.daysLong")}
</b>
.{" "}
{t("paymentPlan.alertDemoEndsIn.selectPlanToAccessFullFeatures")}
</Typography.Text>
}
type="warning"
@ -95,34 +107,104 @@ export default function PaymentPlan() {
}
};
const showAlertPlanCancelled = () => {
if (!isRequesting && requestData.payment_plan_canceled_at) {
return (
<Alert
message={
<Typography.Text>
{t("paymentPlan.alertPlanCancelled", {
date: FormatDatetime(requestData.payment_plan_canceled_at),
})}
</Typography.Text>
}
type="warning"
showIcon
closable
style={{ marginBottom: 12 }}
/>
);
}
};
const percentageDiscount =
requestData.prices.length > 0
? Math.round(
100 -
(100 / (requestData.prices[0].unit_amount * 12)) *
requestData.prices[1].unit_amount
)
: 0;
const handleButtonBuyPlan = () => {
setRequestingCheckout(true);
myFetch({
method: "POST",
url: "/payment/checkout",
body: {
lookupKey: `za-basic-${
selectedBillingPeriod === 0 ? "monthly" : "yearly"
}`,
},
notificationApi,
t,
})
.then((data) => {
if (data.url) {
window.location.href = data.url;
return;
}
if (data.status === "ok") {
fetchPaymentPlan();
setRequestingCheckout(false);
}
})
.catch((error) => {
console.log(error);
setRequestingCheckout(false);
});
};
const ButtonBuyPlan = ({ onClick }) => {
return (
<Button
loading={requestingCheckout}
shape="round"
style={{ fontWeight: "bold" }}
onClick={onClick}
>
{t("paymentPlan.buttonBuyPlan")}
</Button>
);
};
return (
<>
{notificationContextHolder}
{showAlert()}
{showAlertTrialEnds()}
{showAlertPlanCancelled()}
<Card title="Features & Preisübersicht">
<Card title={t("paymentPlan.title")}>
<Row gutter={[16, 16]}>
<Col xs={24} xl={12}>
<Flex vertical>
<Space>
<Check />
<Typography.Text style={{ fontSize: 20 }}>
Unbegrenzte <b>Terminanzahl</b>
</Typography.Text>
</Space>
<Space>
<Check />
<Typography.Text style={{ fontSize: 20 }}>
Bis zu <b>20 Mitarbeiter</b>
</Typography.Text>
</Space>
<Space>
<Check />
<Typography.Text style={{ fontSize: 20 }}>
Buchungen bis zu <b>6 Wochen</b> im Voraus
</Typography.Text>
</Space>
{t("paymentPlan.features", { returnObjects: true }).map(
(feature, index) => (
<Space key={index}>
<Check />
<Typography.Text style={{ fontSize: 20 }}>
<Trans
i18nKey={feature.text}
values={{ bold: feature.bold }}
components={{ b: <b /> }}
/>
</Typography.Text>
</Space>
)
)}
</Flex>
</Col>
@ -140,12 +222,12 @@ export default function PaymentPlan() {
(requestData.payment_plan_canceled_at === undefined ? (
<CheckOutlined />
) : (
<CloseOutlined />
<CloseOutlined style={{ color: "#d10205" }} />
))
}
onClick={() => setSelectedBillingPeriod(0)}
>
Monatlich
{t("paymentPlan.buttonMonthly")}
</Button>
)}
@ -160,12 +242,29 @@ export default function PaymentPlan() {
(requestData.payment_plan_canceled_at === undefined ? (
<CheckOutlined />
) : (
<CloseOutlined />
<CloseOutlined style={{ color: "#d10205" }} />
))
}
onClick={() => setSelectedBillingPeriod(1)}
>
Jährlich
<Space>
<span>{t("paymentPlan.buttonYearly")}</span>
{requestData.payment_plan_interval !== 1 && (
<Tag color="green">
-
<CountUp
start={
percentageDiscount > 10
? percentageDiscount - 10
: 0
}
end={percentageDiscount}
/>{" "}
%
</Tag>
)}
</Space>
</Button>
)}
</Space>
@ -197,74 +296,56 @@ export default function PaymentPlan() {
level={screenBreakpoint.xs ? 3 : 2}
style={{ color: "#fff" }}
>
Unternehmensplan
{t("paymentPlan.businessPlan")}
</Typography.Title>
{isRequesting ? (
<Skeleton.Button shape="round" block active />
) : (
<Button
loading={requestingCheckout}
shape="round"
style={{ fontWeight: "bold" }}
onClick={() => {
setRequestingCheckout(true);
if (
selectedBillingPeriod ===
requestData.payment_plan_interval
) {
myFetch({
method: "GET",
url: "/payment/portal",
})
.then((data) => {
if (data.url) {
window.location.href = data.url;
}
})
.catch((error) => {
console.log(error);
setRequestingCheckout(false);
});
return;
}
myFetch({
method: "POST",
url: "/payment/checkout",
body: {
lookupKey: `za-basic-${
selectedBillingPeriod === 0 ? "monthly" : "yearly"
}`,
},
notificationApi,
t,
})
.then((data) => {
console.log(data);
if (data.url) {
window.location.href = data.url;
return;
}
if (data.status === "ok") {
fetchPaymentPlan();
setRequestingCheckout(false);
}
})
.catch((error) => {
console.log(error);
setRequestingCheckout(false);
});
}}
>
<>
{selectedBillingPeriod ===
requestData.payment_plan_interval
? "Plan verwalten"
: "Jetzt buchen"}
</Button>
requestData.payment_plan_interval ? (
<Button
loading={requestingCheckout}
shape="round"
style={{ fontWeight: "bold" }}
onClick={() => {
setRequestingCheckout(true);
myFetch({
method: "GET",
url: "/payment/portal",
})
.then((data) => {
if (data.url) {
window.location.href = data.url;
}
})
.catch((error) => {
console.log(error);
setRequestingCheckout(false);
});
return;
}}
>
{t("paymentPlan.buttonManagePlan")}
</Button>
) : requestData.payment_plan === 0 ? (
<ButtonBuyPlan onClick={() => handleButtonBuyPlan()} />
) : (
<Popconfirm
title={t("paymentPlan.popconfirmChangePlan.title")}
description={t(
"paymentPlan.popconfirmChangePlan.description"
)}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() => handleButtonBuyPlan()}
>
<ButtonBuyPlan />
</Popconfirm>
)}
</>
)}
</Flex>
@ -296,18 +377,40 @@ export default function PaymentPlan() {
<Typography.Text
style={{ color: "#fff", textAlign: "center" }}
>
/monatlich
/{t("paymentPlan.monthly")}
</Typography.Text>
</Flex>
</Flex>
</Card>
{selectedBillingPeriod === requestData.payment_plan &&
{selectedBillingPeriod === requestData.payment_plan_interval &&
requestData.payment_plan_canceled_at !== undefined && (
<Flex justify="center">
<Typography.Text type="secondary">
Gekündigt am{" "}
{FormatDatetime(requestData.payment_plan_canceled_at)} Uhr
{t("paymentPlan.cancelledOn", {
date: FormatDatetime(
requestData.payment_plan_canceled_at
),
})}
</Typography.Text>
</Flex>
)}
{selectedBillingPeriod === 1 &&
requestData.payment_plan_interval !== 1 && (
<Flex justify="center">
<Typography.Text
type="secondary"
style={{ color: "#27ae60" }}
>
{t("paymentPlan.changeToAnnualPayment")}
<b>
{t("paymentPlan.saveAmount", {
amount:
requestData.prices[0].unit_amount * 12 -
requestData.prices[1].unit_amount,
})}
</b>
</Typography.Text>
</Flex>
)}