diff --git a/index.html b/index.html index 0ac2d657..69b46b21 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,11 @@ + + + + + diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 1ecc882c..aec7ebc9 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -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; } +} \ No newline at end of file diff --git a/public/img/illustrations/undraw_pricing.png b/public/img/illustrations/undraw_pricing.png new file mode 100644 index 00000000..151bb16a Binary files /dev/null and b/public/img/illustrations/undraw_pricing.png differ diff --git a/public/img/illustrations/undraw_pricing.svg b/public/img/illustrations/undraw_pricing.svg new file mode 100644 index 00000000..8966ea28 --- /dev/null +++ b/public/img/illustrations/undraw_pricing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/UserSubscription/Invoice.jsx b/src/components/UserSubscription/Invoice.jsx new file mode 100644 index 00000000..2583a451 --- /dev/null +++ b/src/components/UserSubscription/Invoice.jsx @@ -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 ( +
+ + +
+
+ {/* Header */} +
+
+

INVOICE

+ Payment Receipt +
+
+
+ INV-{payment.paymentId.slice(-8).toUpperCase()} +
+
+
+ Date: {formatDate(payment.createdAt)} +
+
+ Payment ID: {payment.paymentId} +
+
+ + {order.status.toUpperCase()} + +
+
+ +
+ + {/* Billing Details */} +
+
+
Bill To
+

{payment.customerName || "N/A"}

+

{payment.email || "N/A"}

+

{payment.contact || "N/A"}

+
+
+
+ Payment Information +
+

+ Order ID: {order.orderId} +

+

+ Receipt: {order.receipt} +

+

+ Method:{" "} + {payment.method.charAt(0).toUpperCase() + + payment.method.slice(1)} +

+
+
+ +
+ + {/* Transaction Details */} +
+
+ Transaction Details +
+
+
+
+ Payment Status + + {payment.status.charAt(0).toUpperCase() + + payment.status.slice(1)} + +
+
+
+
+ Currency + {payment.currency} +
+
+
+
+ Card Type + + {payment.cardDetails?.cardType || + payment.method.toUpperCase()} + +
+
+
+
+ Last 4 Digits + + {payment.cardDetails?.last4Digits || "N/A"} + +
+
+
+
+ International + + {payment.internationalPayment ? "Yes" : "No"} + +
+
+
+
+ Captured + + {payment.captured ? "Yes" : "No"} + +
+
+
+
+ +
+ + {/* Amount Breakdown */} +
+
+ Payment Summary +
+
+
+ + + + + + + + + + + + + + + + + + + +
Subtotal{formatFigure(subtotal.toFixed(2),{ + type: "currency", + currency: payment.currency, + })}
Processing Fee{formatFigure(payment.fee.toFixed(2),{ + type: "currency", + currency: payment.currency, + })}
Tax{formatFigure(payment.tax.toFixed(2),{ + type: "currency", + currency: payment.currency, + })}
Total Paid + {formatFigure(payment.amount, { + type: "currency", + currency: payment.currency, + })} +
+
+
+
+ +
+ + {/* Footer */} +
+

+ Thank you for your payment! +

+ + This is a computer-generated invoice and does not require a + signature. + + + Generated on:{" "} + {new Date().toLocaleDateString("en-IN", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + })} + +
+
+
+
+ ); +}; + +export default Invoice; diff --git a/src/components/UserSubscription/ProcessedPayment.jsx b/src/components/UserSubscription/ProcessedPayment.jsx new file mode 100644 index 00000000..69f70e3a --- /dev/null +++ b/src/components/UserSubscription/ProcessedPayment.jsx @@ -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 ( +
+
+
+ +
+

Payment Failed!

+

+ Unfortunately, your payment could not be completed. Please try again + or use a different payment method. +

+ +
+ + + Go Back to Dashboard + +
+ + {failPayment?.error && ( +
+ Error Details: +
+                {JSON.stringify(failPayment.error, null, 2)}
+              
+
+ )} +
+
+ ); + } + return ( +
+
+
+
+
+

You’ve Selected the Perfect Plan for Your Organization

+

+ Great choice! This plan is tailored to meet your team’s needs + and help you maximize productivity. +

+
+ {selectedPlan && ( +
+
+
+ {selectedPlan?.planName} + +
+ +
+ + {selectedPlan?.currency?.symbol} {selectedPlan?.price} /{" "} + {frequencyLabel(frequency)} + +
+ + + {selectedPlan?.description} + +
+
+ )} + + {selectedPlan && ( +
+
+ {(() => { + const { + planName, + description, + price, + frequency, + trialDays, + maxUser, + maxStorage, + currency, + features, + } = selectedPlan; + return ( + <> +
+
+
+ + Max Users: {maxUser} +
+
+
+
+ + Max Storage: {maxStorage} MB +
+
+
+
+ + Trial Days: {trialDays} +
+
+
+ +
+ Included Features +
+
+ {features && + Object.entries(features?.modules || {}) + .filter(([key]) => key !== "id") + .map(([key, mod]) => ( +
+ + {mod.name} +
+ ))} +
+ +
+ Support +
+
    + {features?.supports?.emailSupport && ( +
  • + + Email Support +
  • + )} + {features?.supports?.phoneSupport && ( +
  • + + Phone Support +
  • + )} + {features?.supports?.prioritySupport && ( +
  • + + Priority Support +
  • + )} +
+
+
+
+
Duration
+
+ {frequencyLabel(selectedPlan?.frequency, true)} +
+
+ +
+
Total Price
+
+ {formatFigure(selectedPlan?.price, { + type: "currency", + currency: selectedPlan?.currency.currencyCode, + })} +
+
+
+ + ); + })()} +
+
+ )} +
+
+
+ + {client && ( +
+
Confirm your organization details.
+
+
+ Name: +
+
{client.firstName} {client.lastName}
+ + +
+ Email: +
+
{client.email}
+ +
+ Contact Number: +
+
{client.contactNumber}
+ +
+ Organization Name: +
+
{client.organizationName}
+ + + +
+ Onboarding Date: +
+
+ {formatUTCToLocalTime(client.onBoardingDate)} +
+ +
+ Billing Address: +
+
{client.billingAddress}
+ +
+ Industry : +
+
{client?.industry?.name}
+
+
+ )} +
+
+
+ + +
+
+ ); +}; + +export default ProcessedPayment; diff --git a/src/components/UserSubscription/SelectPlan.jsx b/src/components/UserSubscription/SelectPlan.jsx new file mode 100644 index 00000000..7bc3cd56 --- /dev/null +++ b/src/components/UserSubscription/SelectPlan.jsx @@ -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 ( +
+
+
+
+
+

Choose the Perfect Plan for Your Organization

+

+ Select a plan that fits your team’s needs and unlock the + features that drive productivity. +

+
+ + {plans?.map((plan) => ( +
+
+ +
+
+ ))} + + {selectedPlan && ( +
+
+ {(() => { + const selected = plans?.find( + (p) => p.planName === selectedPlan + ); + if (!selected) return null; + + const { + price, + frequency, + trialDays, + maxUser, + maxStorage, + currency, + features, + } = selected; + + return ( + <> +
+
+
+ + Max Users: {maxUser} +
+
+
+
+ + Max Storage: {maxStorage} MB +
+
+
+
+ + Trial Days: {trialDays} +
+
+
+ +
+ Included Features +
+
+ {features && + Object.entries(features?.modules || {}) + .filter(([key]) => key !== "id") + .map(([key, mod]) => ( +
+ + {mod.name} +
+ ))} +
+ +
+ Support +
+
    + {features?.supports?.emailSupport && ( +
  • + + Email Support +
  • + )} + {features?.supports?.phoneSupport && ( +
  • + + Phone Support +
  • + )} + {features?.supports?.prioritySupport && ( +
  • + + Priority Support +
  • + )} +
+ +
+
+
+
Duration
+
+ {frequencyLabel(frequency, true)} +
+
+ +
+
Total Price
+
+ {formatFigure(price, { + type: "currency", + currency: currency.currencyCode, + })} +
+
+
+ + ); + })()} +
+
+ )} +
+
+ + {/* Image Section */} +
+ image +
+
+ +
+ +
+
+ + ); +}; + +export default SelectPlan; diff --git a/src/components/UserSubscription/SubscriptionForm.jsx b/src/components/UserSubscription/SubscriptionForm.jsx new file mode 100644 index 00000000..d9317580 --- /dev/null +++ b/src/components/UserSubscription/SubscriptionForm.jsx @@ -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 ( +
+
+
+
+
+
+
+ {/* First Name */} +
+ + + {errors.firstName && ( +
+ {errors.firstName.message} +
+ )} +
+ + {/* Last Name */} +
+ + + {errors.lastName && ( +
+ {errors.lastName.message} +
+ )} +
+ + {/* Email */} +
+ + + {errors.email && ( +
{errors.email.message}
+ )} +
+ + {/* Contact Number */} +
+ + + {errors.contactNumber && ( +
+ {errors.contactNumber.message} +
+ )} +
+ + {/* Billing Address */} +
+ +