master
alex 2024-03-30 13:47:00 +01:00
parent 1a50324895
commit f646a19cdf
8 changed files with 310 additions and 17 deletions

View File

@ -152,7 +152,7 @@
"privacyPolicyLink": "Datenschutzerklärung",
"loginLink": "Jetzt anmelden",
"signUpLink": "Jetzt registrieren",
"languageSwitchTo": "Switch to",
"languageSwitchTo": "Switch language to",
"login": {
"button": "Anmelden",
"forgotPassword": "Passwort vergessen?",

View File

@ -132,7 +132,6 @@ export function App() {
options={{
username: appUserData.user.username,
permissions: appUserData.permissions,
paymentPlanStatus: appUserData.user.payment_plan_status,
paymentPlanTrialEnd: appUserData.user.payment_plan_trial_end,
paymentPlanCanceledAt: appUserData.user.payment_plan_canceled_at,
}}

View File

@ -7,7 +7,6 @@ import Verification from "../../Pages/Verification";
// Lazy-loaded components
const Authentication = lazy(() => import("../../Pages/Authentication"));
const SignUp = lazy(() => import("../../Pages/Authentication/SignUp"));
const CheckoutSuccess = lazy(() =>
import("../../Pages/Authentication/CheckoutSuccess")
);

View File

@ -106,9 +106,8 @@ export function SideMenuContent({
};
const showPaymentPlanInfoBanner =
sideBarContext.paymentPlanStatus === "trialing" &&
sideBarContext.paymentPlanTrialEnd !== null &&
sideBarContext.paymentPlanCanceledAt !== null;
sideBarContext.paymentPlanTrialEnd !== undefined ||
sideBarContext.paymentPlanCanceledAt !== undefined;
const getSecondMenuItems = () => {
let items = [];
@ -177,7 +176,8 @@ export function SideMenuContent({
const calculateExpiry = () => {
const currentDate = new Date();
const expiryDate = new Date(sideBarContext.paymentPlanTrialEnd * 1000);
const expiryDate = new Date(sideBarContext.paymentPlanTrialEnd);
const diffTime = Math.abs(expiryDate - currentDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

View File

@ -1,11 +1,30 @@
import { Button, notification } from "antd";
import {
Alert,
Button,
Card,
Space,
Flex,
Row,
Col,
Typography,
Grid,
Skeleton,
notification,
} from "antd";
import { useAppContext } from "../../Contexts/AppContext";
import { myFetch, showUnkownErrorNotification } from "../../utils";
import {
AppStyle,
FormatDatetime,
myFetch,
showUnkownErrorNotification,
} from "../../utils";
import { useTranslation } from "react-i18next";
import { CreditCardOutlined } from "@ant-design/icons";
import { useState } from "react";
import PageInDevelopment from "../PageInDevelopment";
// import { ChoosenProduct } from "../Authentication/SignUp";
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";
const { useBreakpoint } = Grid;
export default function PaymentPlan() {
const { t } = useTranslation();
@ -14,13 +33,287 @@ export default function PaymentPlan() {
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [isRequestingBillingDetails, setIsRequestingBillingDetails] =
useState(false);
const screenBreakpoint = useBreakpoint();
const [isRequesting, setIsRequesting] = useState(true);
const [requestData, setRequestData] = useState({
payment_plan: 0,
payment_plan_interval: 0,
prices: [],
payment_plan_trial_end: undefined,
payment_plan_canceled_at: undefined,
});
const [requestingCheckout, setRequestingCheckout] = useState(false);
const [selectedBillingPeriod, setSelectedBillingPeriod] = useState(0);
const fetchPaymentPlan = () => {
myFetch({
url: "/payment",
method: "GET",
notificationApi,
t,
})
.then((data) => {
setRequestData(data);
setIsRequesting(false);
if (data.payment_plan_interval !== null) {
setSelectedBillingPeriod(data.payment_plan_interval);
}
})
.catch((error) => {
console.log(error);
setIsRequesting(false);
showUnkownErrorNotification(notificationApi, t);
});
};
useEffect(() => fetchPaymentPlan(), []);
const showAlert = () => {
if (
!isRequesting &&
requestData.payment_plan === 0 &&
requestData.payment_plan_trial_end &&
!requestData.payment_plan_canceled_at
) {
return (
<Alert
message={
<Typography.Text>
Ihre Demo endet in <b>7 Tagen</b>. Jetzt einen Plan auswählen, um
den vollen Funktionsumfang zu nutzen.
</Typography.Text>
}
type="warning"
closable
showIcon
style={{ marginBottom: 12 }}
/>
);
}
};
return (
<>
{notificationContextHolder}
<PageInDevelopment showBackButton={false} />
{showAlert()}
<Card title="Features & Preisübersicht">
<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>
</Flex>
</Col>
<Col xs={24} xl={12}>
<Flex justify="center" style={{ marginBottom: 12 }}>
<Space>
{isRequesting ? (
<Skeleton.Button shape="round" size="default" active />
) : (
<Button
type={selectedBillingPeriod === 0 ? "primary" : "default"}
shape="round"
icon={
requestData.payment_plan_interval === 0 &&
(requestData.payment_plan_canceled_at === undefined ? (
<CheckOutlined />
) : (
<CloseOutlined />
))
}
onClick={() => setSelectedBillingPeriod(0)}
>
Monatlich
</Button>
)}
{isRequesting ? (
<Skeleton.Button shape="round" size="default" active />
) : (
<Button
type={selectedBillingPeriod === 1 ? "primary" : "default"}
shape="round"
icon={
requestData.payment_plan_interval === 1 &&
(requestData.payment_plan_canceled_at === undefined ? (
<CheckOutlined />
) : (
<CloseOutlined />
))
}
onClick={() => setSelectedBillingPeriod(1)}
>
Jährlich
</Button>
)}
</Space>
</Flex>
<Card style={{ backgroundColor: AppStyle.colors.primary }}>
<Flex
justify="space-between"
vertical={screenBreakpoint.xs}
align="center"
>
<div>
<RocketLaunch
style={{
width: 62,
height: 62,
backgroundColor: "#fff",
padding: 12,
borderRadius: 12,
}}
/>
</div>
<Flex
vertical
style={{ marginBottom: screenBreakpoint.xs && 12 }}
>
<Typography.Title
level={screenBreakpoint.xs ? 3 : 2}
style={{ color: "#fff" }}
>
Unternehmensplan
</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>
)}
</Flex>
<Flex vertical align="center">
{isRequesting ? (
<Skeleton.Button shape="round" size="large" active />
) : (
<Typography.Title
level={screenBreakpoint.xs ? 3 : 2}
style={{
color: "#fff",
padding: 0,
margin: 0,
width: 76,
textAlign: "center",
}}
>
{`${
requestData.prices.find(
(price) =>
price.lookup_key ===
`za-basic-${
selectedBillingPeriod === 0 ? "monthly" : "yearly"
}`
).unit_amount / (selectedBillingPeriod === 0 ? 1 : 12)
} `}
</Typography.Title>
)}
<Typography.Text
style={{ color: "#fff", textAlign: "center" }}
>
/monatlich
</Typography.Text>
</Flex>
</Flex>
</Card>
{selectedBillingPeriod === requestData.payment_plan &&
requestData.payment_plan_canceled_at !== undefined && (
<Flex justify="center">
<Typography.Text type="secondary">
Gekündigt am{" "}
{FormatDatetime(requestData.payment_plan_canceled_at)} Uhr
</Typography.Text>
</Flex>
)}
</Col>
</Row>
</Card>
</>
);
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m98-537 168-168q14-14 33-20t39-2l52 11q-54 64-85 116t-60 126L98-537Zm205 91q23-72 62.5-136T461-702q88-88 201-131.5T873-860q17 98-26 211T716-448q-55 55-120 95.5T459-289L303-446Zm276-120q23 23 56.5 23t56.5-23q23-23 23-56.5T692-679q-23-23-56.5-23T579-679q-23 23-23 56.5t23 56.5ZM551-85l-64-147q74-29 126.5-60T730-377l10 52q4 20-2 39.5T718-252L551-85ZM162-318q35-35 85-35.5t85 34.5q35 35 35 85t-35 85q-25 25-83.5 43T87-74q14-103 32-161t43-83Z"/></svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q65 0 123 19t107 53l-58 59q-38-24-81-37.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160q133 0 226.5-93.5T800-480q0-18-2-36t-6-35l65-65q11 32 17 66t6 70q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-56-216L254-466l56-56 114 114 400-401 56 56-456 457Z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -14,7 +14,7 @@ root.render(
<ConfigProvider
theme={{
token: {
fontFamily: "Roboto, sans-serif",
fontFamily: "Outfit, sans-serif",
colorPrimary: "#1395f8",
colorInfo: "#1395f8",
},