added payment getway using raz razorpay
This commit is contained in:
parent
aa1fc00611
commit
e95a78e1a3
@ -95,6 +95,11 @@
|
||||
|
||||
<!-- Page JS -->
|
||||
<script src="/assets/js/form-wizard-icons.js"></script>
|
||||
<script src="/assets/js/dashboards-analytics.js"></script>
|
||||
|
||||
<!-- Bloack Ui -->
|
||||
<!-- <script src="/assets/js/extended-ui-blockui.js"></script> -->
|
||||
<script src="/assets/js/form-wizard-icons.js"></script>
|
||||
<script src="/assets/js/dashboards-analytics.js"></script>
|
||||
|
||||
<!-- component -->
|
||||
|
||||
@ -3,6 +3,27 @@
|
||||
--bs-nav-link-font-size: 0.7375rem;
|
||||
--bg-border-color :#f8f6f6
|
||||
}
|
||||
/* ===========================% Background_Colors %========================================================== */
|
||||
.bg-light-primary {
|
||||
background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);
|
||||
border:var(--bs-primary-border-subtle)
|
||||
}
|
||||
.bg-light-secondary {
|
||||
background-color: color-mix(in srgb, var(--bs-secondary) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-danger {
|
||||
background-color: color-mix(in srgb, var(--bs-danger) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-success {
|
||||
background-color: color-mix(in srgb, var(--bs-success) 10.4%, transparent);
|
||||
}
|
||||
|
||||
.bg-light-info {
|
||||
background-color: color-mix(in srgb, var(--bs-info) 10.4%, transparent);
|
||||
}
|
||||
.bg-light-warning {
|
||||
background-color: color-mix(in srgb, var(--bs-warning) 10.4%, transparent);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.5rem var(--bs-card-cap-padding-x);
|
||||
@ -25,6 +46,101 @@ font-size: 2rem;
|
||||
.text-md-b {
|
||||
font-weight: normal;
|
||||
}
|
||||
.stepper-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-horizontal {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-point {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #dee2e6;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.timeline-point.completed {
|
||||
background-color: var(--bs-success);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 5px rgba(25, 135, 84, 0.5);
|
||||
}
|
||||
|
||||
.timeline-point.failed {
|
||||
background-color: var(--bs-danger);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
|
||||
}
|
||||
|
||||
.timeline-point.active {
|
||||
background-color: var(--bs-info);
|
||||
color: #fff;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 6px rgba(13, 202, 240, 0.5);
|
||||
}
|
||||
|
||||
.timeline-line-horizontal {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #dee2e6;
|
||||
z-index: 1;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Make line green for completed sections */
|
||||
.timeline-item.completed ~ .timeline-line-horizontal {
|
||||
background-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/* Optional: subtle pulse for active step */
|
||||
.timeline-point.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--bs-info);
|
||||
animation: pulse 1.5s infinite;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.text-xxs { font-size: 0.55rem; } /* 8px */
|
||||
.text-xs { font-size: 0.75rem; } /* 12px */
|
||||
@ -294,3 +410,36 @@ font-weight: normal;
|
||||
.w-8-xl{ width: 2rem; }
|
||||
.w-10-xl{ width: 2.5rem; }
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------Text------------------------- */
|
||||
@media (min-width: 576px) {
|
||||
.fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; }
|
||||
.fs-sm-2 { font-size: calc(1.2rem + 1.2vw) !important; }
|
||||
.fs-sm-3 { font-size: calc(1.1rem + 0.8vw) !important; }
|
||||
.fs-sm-4 { font-size: calc(1rem + 0.5vw) !important; }
|
||||
.fs-sm-5 { font-size: 1.05rem !important; }
|
||||
.fs-sm-6 { font-size: 0.9rem !important; }
|
||||
|
||||
.fs-sm-tiny { font-size: 72% !important; }
|
||||
.fs-sm-big { font-size: 115% !important; }
|
||||
.fs-sm-large { font-size: 155% !important; }
|
||||
.fs-sm-xlarge { font-size: 175% !important; }
|
||||
.fs-sm-xxlarge { font-size: calc(1.6rem + 3.5vw) !important; }
|
||||
}
|
||||
|
||||
/* 💻 Medium devices (≥768px) */
|
||||
@media (min-width: 768px) {
|
||||
.fs-md-1 { font-size: calc(1.4125rem + 1.95vw) !important; }
|
||||
.fs-md-2 { font-size: calc(1.3625rem + 1.35vw) !important; }
|
||||
.fs-md-3 { font-size: calc(1.3rem + 0.6vw) !important; }
|
||||
.fs-md-4 { font-size: calc(1.275rem + 0.3vw) !important; }
|
||||
.fs-md-5 { font-size: 1.125rem !important; }
|
||||
.fs-md-6 { font-size: 0.9375rem !important; }
|
||||
|
||||
.fs-md-tiny { font-size: 70% !important; }
|
||||
.fs-md-big { font-size: 112% !important; }
|
||||
.fs-md-large { font-size: 150% !important; }
|
||||
.fs-md-xlarge { font-size: 170% !important; }
|
||||
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
|
||||
}
|
||||
BIN
public/img/illustrations/undraw_pricing.png
Normal file
BIN
public/img/illustrations/undraw_pricing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
1
public/img/illustrations/undraw_pricing.svg
Normal file
1
public/img/illustrations/undraw_pricing.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
306
src/components/UserSubscription/Invoice.jsx
Normal file
306
src/components/UserSubscription/Invoice.jsx
Normal file
@ -0,0 +1,306 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
import { formatFigure } from "../../utils/appUtils";
|
||||
|
||||
const Invoice = ({ invoiceData, currencySymbol }) => {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const invoiceRef = useRef(null);
|
||||
|
||||
const data = invoiceData || {
|
||||
razorpayPaymentDetails: {
|
||||
amount: 19999,
|
||||
bankDetails: null,
|
||||
captured: true,
|
||||
cardDetails: {
|
||||
cardId: null,
|
||||
cardType: null,
|
||||
emi: false,
|
||||
international: false,
|
||||
issuer: null,
|
||||
last4Digits: null,
|
||||
network: null,
|
||||
subType: null,
|
||||
},
|
||||
contact: "+919145445127",
|
||||
createdAt: "2025-10-25T06:46:30",
|
||||
currency: "INR",
|
||||
description: "",
|
||||
email: "avn18042001@gmail.com",
|
||||
errorCode: "",
|
||||
errorDescription: "",
|
||||
fee: 707.97,
|
||||
internationalPayment: true,
|
||||
method: "card",
|
||||
orderId: "order_RXbzfh8d1X1SSg",
|
||||
paymentId: "pay_RXc08bJHVpjytP",
|
||||
status: "captured",
|
||||
tax: 108,
|
||||
upiDetails: null,
|
||||
walletDetails: null,
|
||||
},
|
||||
razorpayOrderDetails: {
|
||||
amount: 19999,
|
||||
amountDue: 0,
|
||||
amountPaid: 19999,
|
||||
attempts: 1,
|
||||
createdAt: "2025-10-25T06:46:02",
|
||||
currency: "INR",
|
||||
orderId: "order_RXbzfh8d1X1SSg",
|
||||
receipt: "rec_aae16ec3-9571-4c96-bd77-59950fdce236",
|
||||
status: "paid",
|
||||
},
|
||||
};
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
return currencySymbol + amount.toFixed(2);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("en-IN", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const downloadPDF = async () => {
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const invoice = invoiceRef.current;
|
||||
const canvas = await html2canvas(invoice, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: "#ffffff",
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL("image/png");
|
||||
const pdf = new jsPDF({
|
||||
orientation: "portrait",
|
||||
unit: "mm",
|
||||
format: "a4",
|
||||
});
|
||||
|
||||
const imgWidth = 210;
|
||||
const pageHeight = 297;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
|
||||
pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
|
||||
|
||||
const paymentId = data.razorpayPaymentDetails.paymentId;
|
||||
pdf.save(`Invoice_${paymentId}.pdf`);
|
||||
} catch (error) {
|
||||
console.error("Error generating PDF:", error);
|
||||
alert("Failed to generate PDF. Please try again.");
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const payment = data.razorpayPaymentDetails;
|
||||
const order = data.razorpayOrderDetails;
|
||||
const subtotal = payment.amount - payment.fee - payment.tax;
|
||||
|
||||
return (
|
||||
<div className="container-md text-start p-0">
|
||||
<i className="bx bx-download cursor-pointer" onClick={downloadPDF}
|
||||
disabled={isGenerating}></i>
|
||||
|
||||
<div className="invoice-preview p-2" ref={invoiceRef}>
|
||||
<div className="invoice-content">
|
||||
{/* Header */}
|
||||
<div className="row align-items-start mb-4">
|
||||
<div className="col-md-6 d-block text-start align-items-start">
|
||||
<p className="fw-semibold fs-5 m-0 mb-0 text-primary">INVOICE</p>
|
||||
<small className="text-muted">Payment Receipt</small>
|
||||
</div>
|
||||
<div className="col-md-6 text-end">
|
||||
<h6 className="fw-semibold mb-2">
|
||||
INV-{payment.paymentId.slice(-8).toUpperCase()}
|
||||
</h6>
|
||||
<div className="small">
|
||||
<div>
|
||||
<strong>Date:</strong> {formatDate(payment.createdAt)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Payment ID:</strong> {payment.paymentId}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`badge bg-${
|
||||
order.status === "paid" ? "success" : "secondary"
|
||||
} mt-2`}
|
||||
>
|
||||
{order.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Billing Details */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-md-6">
|
||||
<h5 className="fw-semibold border-bottom pb-1 mb-3">Bill To</h5>
|
||||
<p className="mb-1 fw-medium">{payment.customerName || "N/A"}</p>
|
||||
<p className="mb-1">{payment.email || "N/A"}</p>
|
||||
<p className="mb-1">{payment.contact || "N/A"}</p>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<h5 className="fw-semibold border-bottom pb-1 mb-3">
|
||||
Payment Information
|
||||
</h5>
|
||||
<p className="mb-1">
|
||||
<strong>Order ID:</strong> {order.orderId}
|
||||
</p>
|
||||
<p className="mb-1">
|
||||
<strong>Receipt:</strong> {order.receipt}
|
||||
</p>
|
||||
<p className="mb-1">
|
||||
<strong>Method:</strong>{" "}
|
||||
{payment.method.charAt(0).toUpperCase() +
|
||||
payment.method.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Transaction Details */}
|
||||
<div className="mb-4">
|
||||
<h5 className="fw-semibold border-bottom pb-1 mb-3">
|
||||
Transaction Details
|
||||
</h5>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">Payment Status</small>
|
||||
<span className="fw-semibold">
|
||||
{payment.status.charAt(0).toUpperCase() +
|
||||
payment.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">Currency</small>
|
||||
<span className="fw-semibold">{payment.currency}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">Card Type</small>
|
||||
<span className="fw-semibold">
|
||||
{payment.cardDetails?.cardType ||
|
||||
payment.method.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">Last 4 Digits</small>
|
||||
<span className="fw-semibold">
|
||||
{payment.cardDetails?.last4Digits || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">International</small>
|
||||
<span className="fw-semibold">
|
||||
{payment.internationalPayment ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-6">
|
||||
<div className="border rounded p-2 bg-light">
|
||||
<small className="text-muted d-block">Captured</small>
|
||||
<span className="fw-semibold">
|
||||
{payment.captured ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Amount Breakdown */}
|
||||
<div className="mt-3">
|
||||
<h5 className="fw-semibold border-bottom pb-1 mb-3">
|
||||
Payment Summary
|
||||
</h5>
|
||||
<div className="row justify-content-end">
|
||||
<div className="col-md-6">
|
||||
<table className="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="fw-medium">Subtotal</td>
|
||||
<td className="text-end">{formatFigure(subtotal.toFixed(2),{
|
||||
type: "currency",
|
||||
currency: payment.currency,
|
||||
})}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium">Processing Fee</td>
|
||||
<td className="text-end">{formatFigure(payment.fee.toFixed(2),{
|
||||
type: "currency",
|
||||
currency: payment.currency,
|
||||
})}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium">Tax</td>
|
||||
<td className="text-end">{formatFigure(payment.tax.toFixed(2),{
|
||||
type: "currency",
|
||||
currency: payment.currency,
|
||||
})}</td>
|
||||
</tr>
|
||||
<tr className="bg-light-secondary fw-semibold">
|
||||
<td>Total Paid</td>
|
||||
<td className="text-end fw-bold">
|
||||
{formatFigure(payment.amount, {
|
||||
type: "currency",
|
||||
currency: payment.currency,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center mt-4">
|
||||
<p className="mb-1 fw-medium ">
|
||||
Thank you for your payment!
|
||||
</p>
|
||||
<small className="text-muted d-block">
|
||||
This is a computer-generated invoice and does not require a
|
||||
signature.
|
||||
</small>
|
||||
<small className="text-muted d-block mt-2">
|
||||
Generated on:{" "}
|
||||
{new Date().toLocaleDateString("en-IN", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Invoice;
|
||||
358
src/components/UserSubscription/ProcessedPayment.jsx
Normal file
358
src/components/UserSubscription/ProcessedPayment.jsx
Normal file
@ -0,0 +1,358 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { useSubscription } from "../../hooks/useAuth";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
|
||||
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { PaymentRepository } from "../../repositories/PaymentRepository";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelfTenant } from "../../slices/localVariablesSlice";
|
||||
import { unblockUI } from "../../utils/blockUI";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useMakePayment } from "../../hooks/usePayments";
|
||||
import { formatFigure, frequencyLabel } from "../../utils/appUtils";
|
||||
|
||||
const ProcessedPayment = ({
|
||||
onNext,
|
||||
resetPaymentStep,
|
||||
setCurrentStep,
|
||||
setStepStatus,
|
||||
resetFormStep,
|
||||
}) => {
|
||||
const { frequency, planName } = useParams();
|
||||
|
||||
const { details: client, planId: selectedPlanId } = useSelector(
|
||||
(store) => store.localVariables.selfTenant
|
||||
);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [currentPlan, setCurrentPlan] = useState(null);
|
||||
const [failPayment, setFailPayment] = useState(null);
|
||||
|
||||
const {
|
||||
data: plans,
|
||||
isError: isPlanError,
|
||||
isLoading,
|
||||
} = useSubscription(frequency);
|
||||
|
||||
useEffect(() => {
|
||||
if (!plans || !selectedPlanId) return;
|
||||
const selected = plans.find((p) => p.id === selectedPlanId);
|
||||
setSelectedPlan(selected);
|
||||
}, [plans, selectedPlanId]);
|
||||
|
||||
const loadScript = (src) =>
|
||||
new Promise((resolve) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = () => resolve(false);
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
const { mutate: MakePayment, isPending } = useMakePayment(
|
||||
(response) => {
|
||||
|
||||
unblockUI();
|
||||
onNext(response);
|
||||
},
|
||||
(fail) => {
|
||||
|
||||
unblockUI();
|
||||
setFailPayment(fail);
|
||||
onNext(fail);
|
||||
},
|
||||
currentPlan
|
||||
);
|
||||
|
||||
const ProcessToPayment = async () => {
|
||||
setStepStatus((prev) => ({ ...prev, 3: "success" }));
|
||||
setCurrentStep(4);
|
||||
const res = await loadScript(
|
||||
"https://checkout.razorpay.com/v1/checkout.js"
|
||||
);
|
||||
if (!res) {
|
||||
alert("Failed to load Razorpay SDK");
|
||||
return;
|
||||
}
|
||||
MakePayment({ amount: 1 });
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setFailPayment(null);
|
||||
if (typeof resetPaymentStep === "function") resetPaymentStep();
|
||||
};
|
||||
const handlPrevious=()=>{
|
||||
setCurrentStep,
|
||||
setStepStatus((prev) => ({ ...prev, 2: "pending",3:"pending" }));
|
||||
setCurrentStep(2);
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!client || Object.keys(client).length === 0) {
|
||||
// setFailPayment(null);
|
||||
// if (typeof resetFormStep === "function") {
|
||||
// resetFormStep();
|
||||
// }
|
||||
// }
|
||||
// }, [client]);
|
||||
|
||||
if (failPayment) {
|
||||
return (
|
||||
<div className="container-md mt-5 text-center">
|
||||
<div className="d-flex flex-column align-items-center justify-content-center">
|
||||
<div
|
||||
className="bg-danger p-3 rounded-circle mb-3 d-flex align-items-center justify-content-center"
|
||||
style={{ width: "70px", height: "70px" }}
|
||||
>
|
||||
<i className="bx bx-x fs-1 text-white fw-bold"></i>
|
||||
</div>
|
||||
<h4 className="text-danger mb-2">Payment Failed!</h4>
|
||||
<p className="text-muted">
|
||||
Unfortunately, your payment could not be completed. Please try again
|
||||
or use a different payment method.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 d-flex gap-3 flex-column flex-md-row justify-content-center">
|
||||
<button
|
||||
className="btn btn-primary px-4 py-2 fw-semibold"
|
||||
onClick={handleRetry}
|
||||
>
|
||||
Retry Payment
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="btn btn-outline-secondary px-4 py-2 fw-semibold"
|
||||
>
|
||||
Go Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{failPayment?.error && (
|
||||
<div className="alert alert-light-danger mt-4 w-75 mx-auto text-start">
|
||||
<strong>Error Details:</strong>
|
||||
<pre className="small mb-0 mt-2 text-wrap">
|
||||
{JSON.stringify(failPayment.error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="container-sm text-start ">
|
||||
<div className="row gx-1 gy-3 justify-content-between">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-3 text-start">
|
||||
<h4>You’ve Selected the Perfect Plan for Your Organization</h4>
|
||||
<p className="text-muted small mb-3">
|
||||
Great choice! This plan is tailored to meet your team’s needs
|
||||
and help you maximize productivity.
|
||||
</p>
|
||||
</div>
|
||||
{selectedPlan && (
|
||||
<div className="col-12 col-md-8 mb-md-3 mb-2">
|
||||
<div className="custom-option custom-option-basic text-start w-100 bg-light-primary border border-primary shadow-md p-3 rounded-3">
|
||||
<div className="custom-option-header d-flex justify-content-between align-items-center">
|
||||
<span className="h6 mb-0">{selectedPlan?.planName}</span>
|
||||
<i className="bx bx-check-circle text-primary fs-4"></i>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between mt-1">
|
||||
<small className="text-muted">
|
||||
{selectedPlan?.currency?.symbol} {selectedPlan?.price} /{" "}
|
||||
{frequencyLabel(frequency)}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<span className="custom-option-body d-block mt-1">
|
||||
<small>{selectedPlan?.description}</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlan && (
|
||||
<div className="col-12 text-start">
|
||||
<div className="border-warning mt-3">
|
||||
{(() => {
|
||||
const {
|
||||
planName,
|
||||
description,
|
||||
price,
|
||||
frequency,
|
||||
trialDays,
|
||||
maxUser,
|
||||
maxStorage,
|
||||
currency,
|
||||
features,
|
||||
} = selectedPlan;
|
||||
return (
|
||||
<>
|
||||
<div className="row g-2 mb-3">
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-user me-1 text-primary"></i>
|
||||
<strong>Max Users:</strong> {maxUser}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-hdd me-1 text-primary"></i>
|
||||
<strong>Max Storage:</strong> {maxStorage} MB
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-time-five me-1 text-primary"></i>
|
||||
<strong>Trial Days:</strong> {trialDays}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-bold text-secondary mb-2">
|
||||
Included Features
|
||||
</h6>
|
||||
<div className="row ">
|
||||
{features &&
|
||||
Object.entries(features?.modules || {})
|
||||
.filter(([key]) => key !== "id")
|
||||
.map(([key, mod]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="col-4 mb-2 d-flex align-items-center mb-1"
|
||||
>
|
||||
<i
|
||||
className={`fa-regular ${
|
||||
mod.enabled
|
||||
? "fa-circle-check text-success"
|
||||
: "fa-circle-xmark text-danger"
|
||||
}`}
|
||||
></i>
|
||||
<small className="ms-1">{mod.name}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h6 className="fw-bold text-secondary mt-3 mb-1">
|
||||
Support
|
||||
</h6>
|
||||
<ul className="list-unstyled d-flex flex-wrap gap-3 align-items-center mb-0 small">
|
||||
{features?.supports?.emailSupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-envelope text-primary me-1 fs-5"></i>
|
||||
Email Support
|
||||
</li>
|
||||
)}
|
||||
{features?.supports?.phoneSupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-phone text-primary me-1 fs-5"></i>
|
||||
Phone Support
|
||||
</li>
|
||||
)}
|
||||
{features?.supports?.prioritySupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-star text-warning me-1 fs-5"></i>
|
||||
Priority Support
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<hr className="divider border-2 " />
|
||||
<div className="d-flex flex-column co-12 ">
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6 className="fs-6 m-0">Duration</h6>
|
||||
<h5 className="fs-6 m-0">
|
||||
{frequencyLabel(selectedPlan?.frequency, true)}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6 className="fs-4">Total Price</h6>
|
||||
<h5 className="fs-3">
|
||||
{formatFigure(selectedPlan?.price, {
|
||||
type: "currency",
|
||||
currency: selectedPlan?.currency.currencyCode,
|
||||
})}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-4 ">
|
||||
|
||||
{client && (
|
||||
<div className="text-start">
|
||||
<h6 className="fs-md-4 my-4">Confirm your organization details.</h6>
|
||||
<div className="row g-2">
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Name:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client.firstName} {client.lastName}</div>
|
||||
|
||||
|
||||
<div className="col-sm-6">
|
||||
<strong>Email:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client.email}</div>
|
||||
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Contact Number:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client.contactNumber}</div>
|
||||
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Organization Name:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client.organizationName}</div>
|
||||
|
||||
|
||||
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Onboarding Date:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
{formatUTCToLocalTime(client.onBoardingDate)}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Billing Address:</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client.billingAddress}</div>
|
||||
|
||||
<div className="col-sm-6 mb-2">
|
||||
<strong>Industry :</strong>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-2">{client?.industry?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 d-flex justify-content-between">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-label-primary d-flex align-items-center me-2"
|
||||
onClick={handlPrevious}
|
||||
>
|
||||
<i className="bx bx-chevron-left"></i> Previous
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-label-primary d-flex align-items-center me-2"
|
||||
onClick={() => ProcessToPayment(currentPlan?.price)}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Processed To Payment"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessedPayment;
|
||||
238
src/components/UserSubscription/SelectPlan.jsx
Normal file
238
src/components/UserSubscription/SelectPlan.jsx
Normal file
@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSubscription } from "../../hooks/useAuth";
|
||||
import { formatFigure, frequencyLabel } from "../../utils/appUtils";
|
||||
import { setSelfTenant } from "../../slices/localVariablesSlice";
|
||||
|
||||
const SelectPlan = ({ currentStep, setStepStatus, onNext }) => {
|
||||
const { frequency, planName } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const client = useSelector(
|
||||
(store) => store.localVariables.selfTenant.details
|
||||
);
|
||||
const [selectedPlan, setSelectedPlan] = useState(planName);
|
||||
const [currentPlan, setCurrentPlan] = useState(null);
|
||||
const [failPayment, setFailPayment] = useState(null);
|
||||
|
||||
const {
|
||||
data: plans,
|
||||
isError: isPlanError,
|
||||
isLoading,
|
||||
} = useSubscription(frequency);
|
||||
|
||||
const handleChange = (e) => setSelectedPlan(e.target.value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!plans || !selectedPlan) return;
|
||||
const selected = plans.find((p) => p.planName === selectedPlan);
|
||||
if (selected) {
|
||||
setCurrentPlan(selected);
|
||||
dispatch(setSelfTenant({ planId: selected.id }));
|
||||
}
|
||||
}, [plans, selectedPlan, dispatch]);
|
||||
|
||||
const handleNextStep = () => {
|
||||
setStepStatus((prev) => ({ ...prev, 2: "success"}));
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container text-start ">
|
||||
<div className="row gx-4 gy-5 align-items-center justify-content-around">
|
||||
<div className="col-sm-12 col-md-6">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-3 text-start">
|
||||
<h4>Choose the Perfect Plan for Your Organization</h4>
|
||||
<p className="text-muted small mb-3">
|
||||
Select a plan that fits your team’s needs and unlock the
|
||||
features that drive productivity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{plans?.map((plan) => (
|
||||
<div key={plan?.id} className="col-12 col-md-4 mb-md-3 mb-2">
|
||||
<div
|
||||
className={`form-check custom-option custom-option-basic text-start w-100 bg-light-primary ${
|
||||
selectedPlan === plan?.planName
|
||||
? "border border-primary shadow-md"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<label
|
||||
className="form-check-label custom-option-content w-100"
|
||||
htmlFor={`customRadioTemp${plan?.id}`}
|
||||
>
|
||||
<input
|
||||
name="customRadioTemp"
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
value={plan?.planName}
|
||||
id={`customRadioTemp${plan?.id}`}
|
||||
checked={selectedPlan === plan?.planName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="custom-option-header d-flex justify-content-between align-items-center">
|
||||
<span className="h6 mb-0">{plan?.planName}</span>
|
||||
<span>
|
||||
{plan.currency?.symbol} {plan.price} /{" "}
|
||||
{frequencyLabel(frequency)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="custom-option-body d-block mt-1">
|
||||
<small>{plan?.description}</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{selectedPlan && (
|
||||
<div className="col-12 text-start">
|
||||
<div className="border-warning mt-3">
|
||||
{(() => {
|
||||
const selected = plans?.find(
|
||||
(p) => p.planName === selectedPlan
|
||||
);
|
||||
if (!selected) return null;
|
||||
|
||||
const {
|
||||
price,
|
||||
frequency,
|
||||
trialDays,
|
||||
maxUser,
|
||||
maxStorage,
|
||||
currency,
|
||||
features,
|
||||
} = selected;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row g-2 mb-3">
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-user me-1 text-primary"></i>
|
||||
<strong>Max Users:</strong> {maxUser}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-hdd me-1 text-primary"></i>
|
||||
<strong>Max Storage:</strong> {maxStorage} MB
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-md-4">
|
||||
<div className="border rounded-3 p-2 bg-light">
|
||||
<i className="bx bx-time-five me-1 text-primary"></i>
|
||||
<strong>Trial Days:</strong> {trialDays}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-bold text-secondary mb-2">
|
||||
Included Features
|
||||
</h6>
|
||||
<div className="row">
|
||||
{features &&
|
||||
Object.entries(features?.modules || {})
|
||||
.filter(([key]) => key !== "id")
|
||||
.map(([key, mod]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="col-4 mb-2 d-flex align-items-center"
|
||||
>
|
||||
<i
|
||||
className={`fa-regular ${
|
||||
mod.enabled
|
||||
? "fa-circle-check text-success"
|
||||
: "fa-circle-xmark text-danger"
|
||||
}`}
|
||||
></i>
|
||||
<small className="ms-1">{mod.name}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h6 className="fw-bold text-secondary mt-3 mb-2">
|
||||
Support
|
||||
</h6>
|
||||
<ul className="list-unstyled d-flex flex-wrap gap-3 align-items-center mb-0 small">
|
||||
{features?.supports?.emailSupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-envelope text-primary me-1 fs-5"></i>
|
||||
Email Support
|
||||
</li>
|
||||
)}
|
||||
{features?.supports?.phoneSupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-phone text-primary me-1 fs-5"></i>
|
||||
Phone Support
|
||||
</li>
|
||||
)}
|
||||
{features?.supports?.prioritySupport && (
|
||||
<li className="d-flex align-items-center">
|
||||
<i className="bx bx-star text-warning me-1 fs-5"></i>
|
||||
Priority Support
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<hr className="divider border-2" />
|
||||
<div className="d-flex flex-column co-12">
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6 className="fs-6 m-0">Duration</h6>
|
||||
<h5 className="fs-6 m-0">
|
||||
{frequencyLabel(frequency, true)}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6 className="fs-4">Total Price</h6>
|
||||
<h5 className="fs-3">
|
||||
{formatFigure(price, {
|
||||
type: "currency",
|
||||
currency: currency.currencyCode,
|
||||
})}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Section */}
|
||||
<div className="d-none d-md-flex col-md-6 justify-content-center align-items-center">
|
||||
<img
|
||||
src="/public/img/illustrations/undraw_pricing.svg"
|
||||
alt="image"
|
||||
className="img-fluid"
|
||||
style={{
|
||||
maxWidth: "70%",
|
||||
height: "auto",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 d-flex justify-content-end mt-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-label-primary d-flex align-items-center me-2"
|
||||
onClick={handleNextStep}
|
||||
>
|
||||
Next <i className="bx bx-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectPlan;
|
||||
259
src/components/UserSubscription/SubscriptionForm.jsx
Normal file
259
src/components/UserSubscription/SubscriptionForm.jsx
Normal file
@ -0,0 +1,259 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
OrganizationDefaultValue,
|
||||
OrganizationSchema,
|
||||
} from "../../pages/Home/HomeSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Label from "../common/Label";
|
||||
import { orgSize, reference } from "../../utils/constants";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
|
||||
import { useCreateSelfTenant } from "../../hooks/useAuth";
|
||||
import { blockUI } from "../../utils/blockUI";
|
||||
|
||||
const SubscriptionForm = ({ currentStep, setCurrentStep, setStepStatus }) => {
|
||||
const { data, isError, isLoading: industryLoading } = useIndustries();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm({
|
||||
resolver: zodResolver(OrganizationSchema),
|
||||
defaultValues: OrganizationDefaultValue,
|
||||
});
|
||||
|
||||
const { mutate: CreateTenant, isPending } = useCreateSelfTenant(
|
||||
(resp) => {
|
||||
debugger
|
||||
setStepStatus((prev) => ({ ...prev, [currentStep]: "success" }));
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
},
|
||||
(error) => {
|
||||
setStepStatus((prev) => ({ ...prev, [currentStep]: "failed" }));
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = (data) => {
|
||||
CreateTenant(data);
|
||||
// reset();
|
||||
};
|
||||
return (
|
||||
<div className="container-fluid col-md-6 py-3">
|
||||
<div className="d-flex justify-content-center ">
|
||||
<div className="col-12 mt-8">
|
||||
<div className="row px-4">
|
||||
<div className="text-start">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
{/* First Name */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="firstName" required>
|
||||
First Name
|
||||
</Label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("firstName")}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<div className="danger-text">
|
||||
{errors.firstName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="lastName" required>
|
||||
Last Name
|
||||
</Label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("lastName")}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<div className="danger-text">
|
||||
{errors.lastName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="email" required>
|
||||
Email
|
||||
</Label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className="form-control form-control-sm"
|
||||
{...register("email")}
|
||||
/>
|
||||
{errors.email && (
|
||||
<div className="danger-text">{errors.email.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact Number */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="contactNumber" required>
|
||||
Contact Number
|
||||
</Label>
|
||||
<input
|
||||
id="contactNumber"
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("contactNumber")}
|
||||
inputMode="tel"
|
||||
placeholder="+91 9876543210"
|
||||
/>
|
||||
{errors.contactNumber && (
|
||||
<div className="danger-text">
|
||||
{errors.contactNumber.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Billing Address */}
|
||||
<div className="col-12 mb-3">
|
||||
<Label htmlFor="billingAddress" required>
|
||||
Billing Address
|
||||
</Label>
|
||||
<textarea
|
||||
id="billingAddress"
|
||||
className="form-control"
|
||||
{...register("billingAddress")}
|
||||
rows={3}
|
||||
/>
|
||||
{errors.billingAddress && (
|
||||
<div className="danger-text">
|
||||
{errors.billingAddress.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Organization Name */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="organizationName" required>
|
||||
Organization Name
|
||||
</Label>
|
||||
<input
|
||||
id="organizationName"
|
||||
className="form-control form-control-sm"
|
||||
{...register("organizationName")}
|
||||
/>
|
||||
{errors.organizationName && (
|
||||
<div className="danger-text">
|
||||
{errors.organizationName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Organization Size */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="organizationSize" required>
|
||||
Organization Size
|
||||
</Label>
|
||||
<select
|
||||
id="organizationSize"
|
||||
className="form-select shadow-none border py-1 px-2"
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
{...register("organizationSize", {
|
||||
required: "Organization size is required",
|
||||
})}
|
||||
>
|
||||
{orgSize.map((org) => (
|
||||
<option key={org.val} value={org.val}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.organizationSize && (
|
||||
<div className="danger-text">
|
||||
{errors.organizationSize.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Industry */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="industryId" required>
|
||||
Industry
|
||||
</Label>
|
||||
<select
|
||||
id="industryId"
|
||||
className="form-select shadow-none border py-1 px-2 small"
|
||||
{...register("industryId")}
|
||||
>
|
||||
{industryLoading ? (
|
||||
<option value="">Loading...</option>
|
||||
) : (
|
||||
data?.map((indu) => (
|
||||
<option key={indu.id} value={indu.id}>
|
||||
{indu.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.industryId && (
|
||||
<div className="danger-text">
|
||||
{errors.industryId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reference */}
|
||||
<div className="col-sm-6 mb-3">
|
||||
<Label htmlFor="reference" required>
|
||||
Reference
|
||||
</Label>
|
||||
<select
|
||||
id="reference"
|
||||
className="form-select shadow-none border py-1 px-2 small"
|
||||
{...register("reference")}
|
||||
>
|
||||
{reference.map((org) => (
|
||||
<option key={org.val} value={org.val}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.reference && (
|
||||
<div className="danger-text">
|
||||
{errors.reference.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-label-primary d-flex align-items-center me-2"
|
||||
>
|
||||
{isPending ? (
|
||||
<span><i className='bx bx-loader-alt bx-md bx-spin me-2'></i>Please Wait...</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="me-1">Next</span>
|
||||
<i className="bx bx-chevron-right"></i>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionForm;
|
||||
57
src/components/UserSubscription/SubscriptionLayout.jsx
Normal file
57
src/components/UserSubscription/SubscriptionLayout.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
|
||||
const SubscriptionLayout = ({
|
||||
configStep = [],
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
stepStatus = {},
|
||||
}) => {
|
||||
return (
|
||||
<div className="container-fluid stepper-container align-items-start my-4">
|
||||
<ul className="timeline-horizontal list-unstyled d-flex justify-content-between align-items-center position-relative w-100 ">
|
||||
{configStep.map((step, index) => {
|
||||
const stepNumber = index + 1;
|
||||
const status = stepStatus[stepNumber] || "pending";
|
||||
|
||||
return (
|
||||
<li
|
||||
key={step.name}
|
||||
className="timeline-item text-center flex-fill position-relative"
|
||||
>
|
||||
<div
|
||||
className={`timeline-point mx-auto mb-2
|
||||
${status === "success" ? "completed" : ""}
|
||||
${status === "failed" ? "failed" : ""}
|
||||
${stepNumber === currentStep && status === "pending" ? "active" : ""}
|
||||
`}
|
||||
>
|
||||
{status === "failed" ? (
|
||||
<i className="bx bx-x text-white"></i>
|
||||
) : status === "success" ? (
|
||||
<i className="bx bx-check text-white"></i>
|
||||
) : (
|
||||
stepNumber
|
||||
)}
|
||||
</div>
|
||||
|
||||
<small className="fw-semibold fs-sm-6 fs-md-6 mb-0 text-muted">
|
||||
{step.name}
|
||||
</small>
|
||||
|
||||
{index !== configStep.length - 1 && (
|
||||
<div className={`timeline-line-horizontal`}></div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="step-content mt-4">
|
||||
{configStep[currentStep - 1]?.component()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionLayout;
|
||||
|
||||
81
src/components/UserSubscription/VerifiedPayment.jsx
Normal file
81
src/components/UserSubscription/VerifiedPayment.jsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import GlobalModel from "../common/GlobalModel";
|
||||
import Invoice from "./Invoice";
|
||||
|
||||
const VerifiedPayment = ({ onNext, responsePayment }) => {
|
||||
const [isGenerateInvoice, setIsGenerateInvoice] = useState(false);
|
||||
useEffect(() => {
|
||||
if (responsePayment?.success) {
|
||||
onNext();
|
||||
}
|
||||
}, [responsePayment]);
|
||||
if (responsePayment) {
|
||||
return (
|
||||
<div className="container-md mt-5 text-center">
|
||||
<div className="d-flex flex-column align-items-center justify-content-center">
|
||||
<div
|
||||
className="spinner-border text-primary mb-3 p-1"
|
||||
role="status"
|
||||
></div>
|
||||
<h4 className="text-primary mb-2">Verifying payment...</h4>
|
||||
<p className="text-muted">
|
||||
Please wait while we verify your transaction. Do not refresh or
|
||||
close this page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!responsePayment?.success) {
|
||||
|
||||
return (
|
||||
<div className="container-md mt-3 text-center h-auto">
|
||||
{isGenerateInvoice && (
|
||||
<GlobalModel
|
||||
isOpen={isGenerateInvoice}
|
||||
closeModal={() => setIsGenerateInvoice(false)}
|
||||
>
|
||||
<Invoice invoiceData={responsePayment?.data} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<span className="bg-success p-2 p-md-3 rounded-circle">
|
||||
<i className="bx bx-check fs-2 fw-bold text-white"></i>
|
||||
</span>
|
||||
<span className="fs-3 fs-md-2 ms-3 text-success">
|
||||
Payment Successful!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-muted mb-4 fs-6 fs-md-5 text-center mt-8">
|
||||
Thank you for your payment. Your <strong>subscription</strong> has
|
||||
been successfully activated.
|
||||
</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<small className="text-muted ">
|
||||
A Set Password link has been sent to your registered email address .
|
||||
Please check your inbox .
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column flex-md-row justify-content-center gap-3 my-12 ">
|
||||
<a href="/" className="btn btn-info px-4 py-2 fw-semibold">
|
||||
Go to Dashboard
|
||||
</a>
|
||||
<button
|
||||
className="btn btn-outline-primary px-4 py-2 fw-semibold"
|
||||
onClick={() => setIsGenerateInvoice(true)}
|
||||
>
|
||||
Generate Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default VerifiedPayment;
|
||||
@ -13,6 +13,7 @@ import {
|
||||
closeModal,
|
||||
openAuthModal,
|
||||
openModal,
|
||||
setSelfTenant,
|
||||
toggleModal,
|
||||
} from "../slices/localVariablesSlice.jsx";
|
||||
import { removeSession } from "../utils/authUtils.js";
|
||||
@ -82,6 +83,33 @@ export const useSelectTenant = (onSuccessCallBack) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useCreateSelfTenant = (onSuccessCallBack, onFailureCallBack) => {
|
||||
const dispatch = useDispatch();
|
||||
return useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
const resp = await AuthRepository.createSuscription(payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: (response, variables) => {
|
||||
debugger
|
||||
dispatch(
|
||||
setSelfTenant({
|
||||
tenantEnquireId: response?.id,
|
||||
planId: null,
|
||||
details:response
|
||||
})
|
||||
);
|
||||
debugger
|
||||
if (onSuccessCallBack) onSuccessCallBack(response);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast("Somthing worng went happend", "error");
|
||||
if (onFailureCallBack) onFailureCallBack();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { isOpen } = useSelector((state) => state.localVariables.AuthModal);
|
||||
|
||||
@ -1,35 +1,181 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { PaymentRepository } from "../repositories/PaymentRepository";
|
||||
import showToast from "../services/toastService";
|
||||
import { useSelector } from "react-redux";
|
||||
import { blockUI, unblockUI } from "../utils/blockUI";
|
||||
|
||||
export const useMakePayment = (onSuccessCallBack) => {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (payload) => PaymentRepository.makePayment(payload),
|
||||
onSuccess: (_, varibales) => {
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.message ||
|
||||
error.response.message ||
|
||||
"Something went wrong.Please try again later.",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useVerifyPayment = () => {
|
||||
export const removeRazorpayArtifacts=()=> {
|
||||
try {
|
||||
document
|
||||
.querySelectorAll("iframe[src*='razorpay'], iframe[name^='__PRIVATE']")
|
||||
.forEach((iframe) => iframe.remove());
|
||||
|
||||
document
|
||||
.querySelectorAll(
|
||||
"div.razorpay-container, div[class*='razorpay-backdrop'], div[style*='z-index: 1040'], div[style*='z-index: 9999']"
|
||||
)
|
||||
.forEach((el) => el.remove());
|
||||
Array.from(document.querySelectorAll("body > div")).forEach((div) => {
|
||||
const html = div.outerHTML || "";
|
||||
if (
|
||||
html.includes("razorpay-container") ||
|
||||
html.includes("Test Mode") ||
|
||||
html.includes("razorpay-backdrop")
|
||||
) {
|
||||
div.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.removeAttribute("style");
|
||||
document.body.style.overflow = "";
|
||||
document.body.style.position = "";
|
||||
document.body.style.height = "";
|
||||
document.body.style.pointerEvents = "auto";
|
||||
|
||||
document.documentElement.style.overflow = "";
|
||||
document.documentElement.style.removeProperty("overflow");
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
} catch (err) {
|
||||
console.warn(" Error while cleaning Razorpay artifacts:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const closeRazorpayPopup=()=> {
|
||||
try {
|
||||
if (window.Razorpay && typeof window.Razorpay.close === "function") {
|
||||
window.Razorpay.close();
|
||||
}
|
||||
|
||||
setTimeout(removeRazorpayArtifacts, 600);
|
||||
} catch (err) {
|
||||
console.warn(" Error closing Razorpay popup:", err);
|
||||
removeRazorpayArtifacts();
|
||||
}
|
||||
}
|
||||
|
||||
export const useVerifyPayment = (onSuccessCallBack, onFailureCallBack) => {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload) => PaymentRepository.verifyPayment(payload),
|
||||
onSuccess: (_, varibales) => {
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
|
||||
onSuccess: (data) => {
|
||||
if (onSuccessCallBack) onSuccessCallBack(data);
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
if (onFailureCallBack) onFailureCallBack(error);
|
||||
showToast(
|
||||
error.message ||
|
||||
error.response.message ||
|
||||
"Something went wrong.Please try again later.",
|
||||
error?.message ||
|
||||
error?.response?.message ||
|
||||
"Something went wrong. Please try again later.",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useMakePayment = (
|
||||
onSuccessCallBack,
|
||||
onFailureCallBack,
|
||||
currentPlan
|
||||
) => {
|
||||
const client = useQueryClient();
|
||||
const { tenantEnquireId, planId } = useSelector(
|
||||
(store) => store.localVariables.selfTenant
|
||||
);
|
||||
|
||||
const { mutate: verifyPayment } = useVerifyPayment(
|
||||
(response) => onSuccessCallBack?.(response),
|
||||
(error) => onFailureCallBack?.(error)
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload) => PaymentRepository.makePayment(payload),
|
||||
|
||||
onSuccess: (data) => {
|
||||
const orderId = data?.data?.orderId;
|
||||
const key = data?.data?.key;
|
||||
|
||||
if (!orderId || !key) {
|
||||
showToast("Invalid Razorpay order details.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
let manuallyClosed = false;
|
||||
|
||||
const options = {
|
||||
key,
|
||||
amount: (currentPlan?.amount ?? 1) * 100,
|
||||
currency: currentPlan?.currency?.currencyCode || "INR",
|
||||
name: "MarcoAIOT Subscription",
|
||||
order_id: orderId,
|
||||
|
||||
handler: async (response) => {
|
||||
if (manuallyClosed) {
|
||||
unblockUI()
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
tenantEnquireId,
|
||||
planId,
|
||||
orderId: response.razorpay_order_id,
|
||||
paymentId: response.razorpay_payment_id,
|
||||
signature: response.razorpay_signature,
|
||||
};
|
||||
verifyPayment(payload);
|
||||
} finally {
|
||||
closeRazorpayPopup();
|
||||
}
|
||||
},
|
||||
|
||||
prefill: {
|
||||
name: "",
|
||||
email: "",
|
||||
contact: "",
|
||||
},
|
||||
|
||||
theme: { color: "#ea3b0fff" },
|
||||
|
||||
modal: {
|
||||
ondismiss: () => {
|
||||
manuallyClosed = true;
|
||||
unblockUI();
|
||||
closeRazorpayPopup();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const razorpay = new window.Razorpay(options);
|
||||
|
||||
razorpay.on("payment.failed", (response) => {
|
||||
if (manuallyClosed) return;
|
||||
onFailureCallBack?.({
|
||||
status: "failed",
|
||||
message: response.error?.description || "Payment failed.",
|
||||
error: response.error,
|
||||
reason: "transaction_failed",
|
||||
});
|
||||
closeRazorpayPopup();
|
||||
});
|
||||
|
||||
blockUI("Please Wait...");
|
||||
razorpay.open();
|
||||
} catch (err) {
|
||||
alert("This browser is not supported. Please try another browser.");
|
||||
closeRazorpayPopup();
|
||||
}
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.message ||
|
||||
error?.response?.message ||
|
||||
"Something went wrong. Please try again later.",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
|
||||
27
src/pages/Home/HomeSchema.jsx
Normal file
27
src/pages/Home/HomeSchema.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
firstName: z.string().min(1, "First Name is required"),
|
||||
lastName: z.string().min(1, "Last Name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
billingAddress: z.string().min(1, "Billing Address is required"),
|
||||
organizationName: z.string().min(1, "Organization Name is required"),
|
||||
contactNumber: z
|
||||
.string()
|
||||
.min(5, "Contact Number is too short")
|
||||
.regex(
|
||||
/^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/,
|
||||
"Invalid phone number format"
|
||||
),
|
||||
// onBoardingDate: z.coerce
|
||||
// .date()
|
||||
// .refine((d) => !Number.isNaN(d.getTime()), { message: "Invalid date" }),
|
||||
organizationSize: z.string().min(1, "Organization Size is required"),
|
||||
industryId: z.string().uuid("Industry is required"),
|
||||
reference: z.string().min(1,{message:"Reference is required"}),
|
||||
});
|
||||
|
||||
|
||||
export const OrganizationDefaultValue = {
|
||||
|
||||
}
|
||||
127
src/pages/Home/MakeSubscription.jsx
Normal file
127
src/pages/Home/MakeSubscription.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
|
||||
import SubscriptionLayout from "../../components/UserSubscription/SubscriptionLayout";
|
||||
import SubscriptionForm from "../../components/UserSubscription/SubscriptionForm";
|
||||
import ProcessedPayment from "../../components/UserSubscription/ProcessedPayment";
|
||||
import VerifiedPayment from "../../components/UserSubscription/VerifiedPayment";
|
||||
import SelectPlan from "../../components/UserSubscription/SelectPlan";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const MakeSubscription = () => {
|
||||
const [currentStep, setCurrentStep] = useState(2);
|
||||
const [responsePayment, setResponsePayment] = useState(null);
|
||||
|
||||
const [stepStatus, setStepStatus] = useState({
|
||||
1: "pending",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
4: "pending",
|
||||
5: "pending",
|
||||
});
|
||||
|
||||
const handleVerification = (resp) => {
|
||||
setResponsePayment(resp);
|
||||
if (resp?.success) {
|
||||
setStepStatus((prev) => ({ ...prev, 4: "success" }));
|
||||
setCurrentStep(5);
|
||||
} else {
|
||||
setStepStatus((prev) => ({ ...prev, 4: "failed" }));
|
||||
}
|
||||
};
|
||||
const handleNext = () => {
|
||||
setStepStatus((prev) => ({
|
||||
...prev,
|
||||
[currentStep]: "success",
|
||||
[currentStep + 1]: "pending",
|
||||
}));
|
||||
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const checkOut_Steps = [
|
||||
{
|
||||
name: "Client Info",
|
||||
component: () => (
|
||||
<SubscriptionForm
|
||||
currentStep={currentStep}
|
||||
setCurrentStep={setCurrentStep}
|
||||
setStepStatus={setStepStatus}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Select Plan",
|
||||
component: () => (
|
||||
<SelectPlan
|
||||
currentStep={currentStep}
|
||||
setStepStatus={setStepStatus}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Review",
|
||||
component: () => (
|
||||
<ProcessedPayment
|
||||
onNext={(resp) => handleVerification(resp)}
|
||||
resetPaymentStep={() =>
|
||||
setStepStatus((prev) => ({ ...prev, 4: "pending" }))
|
||||
}
|
||||
setCurrentStep={setCurrentStep}
|
||||
setStepStatus={setStepStatus}
|
||||
resetFormStep={() => {
|
||||
setStepStatus((prev) => ({ ...prev, 1: "pending" }));
|
||||
setCurrentStep(1);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Payment",
|
||||
component: () => (
|
||||
<ProcessedPayment
|
||||
onNext={(resp) => handleVerification(resp)}
|
||||
resetPaymentStep={() =>
|
||||
setStepStatus((prev) => ({ ...prev, 4: "pending" }))
|
||||
}
|
||||
setCurrentStep={setCurrentStep}
|
||||
setStepStatus={setStepStatus}
|
||||
resetFormStep={() => {
|
||||
setStepStatus((prev) => ({ ...prev, 1: "pending" }));
|
||||
setCurrentStep(1);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Verified",
|
||||
component: () => (
|
||||
<VerifiedPayment
|
||||
onNext={() => {
|
||||
setStepStatus((prev) => ({ ...prev, 5: "success" }));
|
||||
}}
|
||||
responsePayment={responsePayment}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container-fluid">
|
||||
|
||||
|
||||
<div className="d-block align-items-start mt-6">
|
||||
<SubscriptionLayout
|
||||
configStep={checkOut_Steps}
|
||||
currentStep={currentStep}
|
||||
setCurrentStep={setCurrentStep}
|
||||
stepStatus={stepStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakeSubscription;
|
||||
@ -124,14 +124,20 @@ const SubscriptionPlans = () => {
|
||||
</ul>
|
||||
|
||||
{/* Button */}
|
||||
<div className="mt-auto">
|
||||
<Link
|
||||
to="/auth/reqest/demo"
|
||||
className="btn btn-outline-primary w-100 fw-bold"
|
||||
>
|
||||
Request a Demo
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<Link
|
||||
to={`/auth/subscripe/${frequency}/${plan.planName}`}
|
||||
className="btn btn-outline-primary w-100 fw-bold mb-2"
|
||||
>
|
||||
Subscribe
|
||||
</Link>
|
||||
<Link
|
||||
to="/auth/reqest/demo"
|
||||
className="btn btn-outline-primary w-100 fw-bold"
|
||||
>
|
||||
Request a Demo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@ -11,6 +11,7 @@ const AuthRepository = {
|
||||
register: (data) => api.postPublic("/api/auth/register", data),
|
||||
sendMail: (data) => api.postPublic("/api/auth/sendmail", data),
|
||||
getSubscription:(frequency)=> api.getPublic(`/api/market/list/subscription-plan?frequency=${frequency}`),
|
||||
createSuscription:(data)=>api.post(`/api/Tenant/self/create`,data),
|
||||
|
||||
// Protected routes (require auth token)
|
||||
logout: (data) => api.post("/api/auth/logout", data),
|
||||
|
||||
@ -55,6 +55,7 @@ import { ComingSoonPage } from "../pages/Misc/ComingSoonPage";
|
||||
import ImageGalleryPage from "../pages/Gallary/ImageGallaryPage";
|
||||
import CollectionPage from "../pages/collections/CollectionPage";
|
||||
import SubscriptionSummary from "../pages/Home/SubscriptionSummary";
|
||||
import MakeSubscription from "../pages/Home/MakeSubscription";
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
@ -75,7 +76,7 @@ const router = createBrowserRouter(
|
||||
],
|
||||
},
|
||||
{ path: "/auth/switch/org", element: <TenantSelectionPage /> },
|
||||
{ path: "/request", element: <SubscriptionSummary /> },
|
||||
{ path: "/auth/subscripe/:frequency/:planName", element: <MakeSubscription /> },
|
||||
{
|
||||
element: <ProtectedRoute />,
|
||||
errorElement: <ErrorPage />,
|
||||
|
||||
@ -31,6 +31,11 @@ const localVariablesSlice = createSlice({
|
||||
AuthModal: {
|
||||
isOpen: false,
|
||||
},
|
||||
selfTenant: {
|
||||
tenantEnquireId: null,
|
||||
planId: null,
|
||||
details:null,
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
changeMaster: (state, action) => {
|
||||
@ -96,6 +101,14 @@ const localVariablesSlice = createSlice({
|
||||
const { modalType } = action.payload;
|
||||
state.modals[modalType].isOpen = !state.modals[modalType].isOpen;
|
||||
},
|
||||
setSelfTenant: (state, action) => {
|
||||
state.selfTenant.tenantEnquireId =
|
||||
action.payload.tenantEnquireId ?? state.selfTenant.tenantEnquireId;
|
||||
state.selfTenant.planId =
|
||||
action.payload.planId ?? state.selfTenant.planId;
|
||||
state.selfTenant.details =
|
||||
action.payload.details ?? state.selfTenant.details;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -110,6 +123,6 @@ export const {
|
||||
toggleOrgModal,
|
||||
openAuthModal,
|
||||
closeAuthModal,
|
||||
setOrganization,openModal, closeModal, toggleModal
|
||||
setOrganization,openModal, closeModal, toggleModal , setSelfTenant
|
||||
} = localVariablesSlice.actions;
|
||||
export default localVariablesSlice.reducer;
|
||||
|
||||
@ -135,3 +135,17 @@ export const formatFigure = (
|
||||
|
||||
return new Intl.NumberFormat(locale, formatterOptions).format(amount);
|
||||
};
|
||||
export const frequencyLabel = (freq,isLong=false) => {
|
||||
switch (freq) {
|
||||
case 0:
|
||||
return isLong ? "1 Month" : "1 mo";
|
||||
case 1:
|
||||
return isLong ? "3 Months" : "3 mo";
|
||||
case 2:
|
||||
return isLong ? "6 Months" : "6 mo";
|
||||
case 3:
|
||||
return isLong ? "1 Year" : "1 yr";
|
||||
default:
|
||||
return "mo";
|
||||
}
|
||||
};
|
||||
32
src/utils/blockUI.js
Normal file
32
src/utils/blockUI.js
Normal file
@ -0,0 +1,32 @@
|
||||
export const blockUI = (message = 'Please wait...') => {
|
||||
if (window.$ && window.$.blockUI) {
|
||||
window.$.blockUI({
|
||||
message: `
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div class="sk-wave mb-2">
|
||||
<div class="sk-wave-rect"></div>
|
||||
<div class="sk-wave-rect"></div>
|
||||
<div class="sk-wave-rect"></div>
|
||||
<div class="sk-wave-rect"></div>
|
||||
<div class="sk-wave-rect"></div>
|
||||
</div>
|
||||
<p class="text-white">${message}</p>
|
||||
</div>`,
|
||||
css: {
|
||||
backgroundColor: 'transparent',
|
||||
border: '0',
|
||||
color: '#fff',
|
||||
},
|
||||
overlayCSS: {
|
||||
opacity: 0.5,
|
||||
cursor: 'wait',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const unblockUI = () => {
|
||||
if (window.$ && window.$.unblockUI) {
|
||||
window.$.unblockUI();
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user