handle upgrad plan

This commit is contained in:
pramod mahajan 2025-08-14 17:45:01 +05:30
parent b9bd4e7e18
commit 0acd733710
9 changed files with 260 additions and 85 deletions

View File

@ -12,7 +12,7 @@ const Profile = ({ data }) => {
<div className="container-fuid"> <div className="container-fuid">
<div className="row"> <div className="row">
<div className="col-12 "> <div className="col-12 my-2">
<div className="d-flex flex-wrap align-items-start position-relative "> <div className="d-flex flex-wrap align-items-start position-relative ">
<div className=" d-flex align-items-start gap-2"> <div className=" d-flex align-items-start gap-2">
<img <img
@ -32,20 +32,23 @@ const Profile = ({ data }) => {
className="position-absolute top-0 end-0 cursor-auto" className="position-absolute top-0 end-0 cursor-auto"
onClick={() => setEditTenant(true)} onClick={() => setEditTenant(true)}
> >
<i className="bx bx-edit bs-sm text-primary"></i> <i className="bx bx-edit bs-sm text-primary cursor-pointer"></i>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div className="divider text-start">
<div className="divider-text">Profile</div>
</div>
{data?.description && ( {data?.description && (
<div className="col rounded-2 bg-light justify-content-start p-2"> <div className="col rounded-2 bg-light justify-content-start p-2">
<p className="m-0">{data?.description}</p> <p className="m-0">{data?.description}</p>
</div> </div>
)} )}
</div>
<div className="divider text-start my-1">
<div className="divider-text">Personal</div>
</div>
<div className="row "> <div className="row ">
<div className="col-12 col-md-6 d-flex align-items-center"> <div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-user me-1"></i> <i className="bx bx-sm bx-user me-1"></i>
@ -64,56 +67,55 @@ const Profile = ({ data }) => {
<span className="ms-2">{data.contactNumber}</span> <span className="ms-2">{data.contactNumber}</span>
</div> </div>
<div className="col-12 d-flex text-wrap align-items-start my-4 m-0"> <div className="col-12 d-flex text-wrap align-items-start my-2 m-0">
<i className="bx bx-sm bx-mark me-1"></i> <i className="bx bx-sm bx-mark me-1"></i>
<span className="fw-semibold">Address:</span> <span className="fw-semibold">Address:</span>
<span className="ms-2">{data.billingAddress}</span> <span className="ms-2">{data.billingAddress}</span>
</div> </div>
</div> </div>
</div> <div className="divider text-start ">
<div className="divider text-start">
<div className="divider-text">Organization</div> <div className="divider-text">Organization</div>
</div> </div>
<div className="col-12 d-flex align-items-center"> <div className="col-12 d-flex align-items-center">
<i class="bx bx-sm bxs-building"></i> <i className="bx bx-sm bxs-building"></i>
<span className="fw-semibold">Industry:</span> <span className="fw-semibold">Industry:</span>
<span className="ms-2">{data.industry.name}</span> <span className="ms-2">{data?.industry?.name}</span>
</div> </div>
<div className="row "> <div className="row ">
{data.taxId && ( {data?.taxId && (
<div className="col-12 col-md-6 d-flex align-items-center"> <div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-id-card me-1"></i> <i className="bx bx-sm bx-id-card me-1"></i>
<span className="fw-semibold">Tax Id:</span> <span className="fw-semibold">Tax Id:</span>
<span className="ms-2">{data.taxId}</span> <span className="ms-2">{data?.taxId}</span>
</div> </div>
)} )}
<div className="col-12 col-md-6 d-flex align-items-center my-4 m-0"> <div className="col-12 col-md-6 d-flex align-items-center my-4 m-0">
<i className="bx bx-sm bx-group me-1"></i> <i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Organization Size:</span> <span className="fw-semibold">Organization Size:</span>
<span className="ms-2">{data.organizationSize}</span> <span className="ms-2">{data?.organizationSize}</span>
</div> </div>
<div className="col-12 col-md-6 d-flex align-items-center"> <div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bxs-calendar"></i> <i className="bx bx-sm bxs-calendar"></i>
<span className="fw-semibold">On-Boarding Date:</span> <span className="fw-semibold">On-Boarding Date:</span>
<span className="ms-2"> <span className="ms-2">
{formatUTCToLocalTime(data.onBoardingDate)} {formatUTCToLocalTime(data?.onBoardingDate)}
</span> </span>
</div> </div>
<table className="table table-bordered text-center text-nowrap table-responsive my-4"> <table className="table table-bordered text-center text-nowrap table-responsive my-4">
<tbody> <tbody>
<tr> <tr>
<td colspan="1"> <td colSpan="1">
<strong>Status</strong> <strong>Status</strong>
</td> </td>
<td colspan="1"> <td colSpan="1">
<strong>Active</strong> <strong>Active</strong>
</td> </td>
<td colspan="1"> <td colSpan="1">
<strong>In-Progress</strong> <strong>In-Progress</strong>
</td> </td>
<td colspan="1"> <td colSpan="1">
<strong>On Hold</strong> <strong>On Hold</strong>
</td> </td>
<td> <td>
@ -150,13 +152,13 @@ const Profile = ({ data }) => {
<div className="col-12 col-md-6 d-flex align-items-center"> <div className="col-12 col-md-6 d-flex align-items-center">
<i className="bx bx-sm bx-group me-1"></i> <i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">Activite Employees:</span> <span className="fw-semibold">Activite Employees:</span>
<span className="ms-2">{data.activeEmployees}</span> <span className="ms-2">{data?.activeEmployees}</span>
</div> </div>
<div className="col-12 col-md-6 d-flex align-items-center my-4 m-0"> <div className="col-12 col-md-6 d-flex align-items-center my-4 m-0">
<i className="bx bx-sm bx-group me-1"></i> <i className="bx bx-sm bx-group me-1"></i>
<span className="fw-semibold">In-Active Employee:</span> <span className="fw-semibold">In-Active Employee:</span>
<span className="ms-2">{data.inActiveEmployees}</span> <span className="ms-2">{data?.inActiveEmployees}</span>
</div> </div>
</div> </div>
</div> </div>
@ -165,6 +167,4 @@ const Profile = ({ data }) => {
}; };
export default Profile; export default Profile;
// <span className="badge text-bg-primary position-absolute top-0 end-0">
// {data?.tenantStatus?.name}
// </span>

View File

@ -11,7 +11,7 @@ const SubScription = ({ onSubmitSubScription, onNext }) => {
const [frequency, setFrequency] = useState(2); const [frequency, setFrequency] = useState(2);
const [selectedPlanId, setSelectedPlanId] = useState(null); const [selectedPlanId, setSelectedPlanId] = useState(null);
const selectedTenant = useSelector( const selectedTenant = useSelector(
(store) => store.globalVariables.currentTenant (store) => store.globalVariables.currentTenant?.data
); );
const naviget = useNavigate(); const naviget = useNavigate();
const { const {
@ -33,7 +33,7 @@ const SubScription = ({ onSubmitSubScription, onNext }) => {
isPending, isPending,
error, error,
} = useAddSubscription(() => { } = useAddSubscription(() => {
onNext(); naviget("/tenants")
}); });
const handleSubscriptionSubmit = async () => { const handleSubscriptionSubmit = async () => {
const isValid = await trigger([ const isValid = await trigger([

View File

@ -0,0 +1,111 @@
import React, { useEffect } from "react";
import { useTenantDetails } from "../../hooks/useTenant";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { useNavigate } from "react-router-dom";
const SubScriptionHistory = ({ tenantId }) => {
const { data, isLoading, isError, error } = useTenantDetails(tenantId);
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
if (data) {
// Tenant exists set operationMode: 1
dispatch(setCurrentTenant({ operationMode: 1, data }));
} else {
// No tenant yet set operationMode: 0
dispatch(setCurrentTenant({ operationMode: 0, data: null }));
}
}, [data, dispatch]);
const handleUpgradePlan = () => {
navigate("/tenants/new-tenant");
};
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error}</div>;
const plan = data?.currentPlan;
// No subscription plan yet show "Add Subscription" button
if (!plan) {
return (
<div className="text-center">
<button className="btn btn-sm btn-success" onClick={handleUpgradePlan}>
Add Subscription
</button>
<div className="mt-2 text-center small">
<i className="bx bx-info-circle bx-xs"></i> Add your new subscription
</div>
</div>
);
}
// Subscription plan exists show details
const today = new Date();
const start = new Date(plan.startDate);
const end = new Date(plan.endDate);
const totalDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
const daysLeft = Math.max(0, Math.ceil((end - today) / (1000 * 60 * 60 * 24)));
const percentage = Math.min(
100,
Math.round(((totalDays - daysLeft) / totalDays) * 100)
);
const getProgressVariant = () => {
if (percentage < 50) return "success";
if (percentage < 80) return "warning";
return "danger";
};
return (
<div className="container-fluid p-2">
<div className="text-start mb-3">
<p className="fw-bold">Active Subscription</p>
<div className="row">
<div className="col-12 col-sm-8 border rounded p-3 shadow-sm">
<p className="text-primary fw-bold m-0">{plan.planName || "N/A"}</p>
<div className="d-flex justify-content-between align-items-end mt-2">
<div>
<h3 className="m-0">
{plan.currency?.symbol} {plan.price}
</h3>
<small className="text-muted">{plan.description}</small>
</div>
<div>
<button
className="btn btn-sm btn-success"
onClick={handleUpgradePlan}
>
Upgrade Plan
</button>
</div>
</div>
<div className="mt-3">
{/* Progress bar placeholder */}
<div className="d-flex justify-content-between small text-muted mt-1">
<span>{totalDays - daysLeft} days used</span>
<span>{daysLeft} days left</span>
</div>
</div>
<div className="text-end mt-1 small text-muted">
Ends on {end.toLocaleDateString()}
</div>
</div>
</div>
<div className="text-start">
{/* Table */}
</div>
</div>
</div>
);
};
export default SubScriptionHistory;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import ContactInfro from "./ContactInfro"; import ContactInfro from "./ContactInfro";
import SubScription from "./SubScription"; import SubScription from "./SubScription";
import OrganizationInfo from "./OrganizationInfo"; import OrganizationInfo from "./OrganizationInfo";
@ -12,12 +12,33 @@ import {
tenantDefaultValues, tenantDefaultValues,
} from "./TenantSchema"; } from "./TenantSchema";
import Congratulation from "./congratulation"; import Congratulation from "./congratulation";
import { useSelector } from "react-redux";
const TenantForm = () => { const TenantForm = () => {
const HasSelectedCurrentTenant = useSelector(
(store) => store.globalVariables.currentTenant
);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [completedTabs, setCompletedTabs] = useState([]); const [completedTabs, setCompletedTabs] = useState([]);
const PlanTextLabel = HasSelectedCurrentTenant?.operationMode === 1 ? "Upgrade Plan":"Select Plan"
// Jump to subscription if tenant already exists
useEffect(() => {
if (HasSelectedCurrentTenant) {
if (HasSelectedCurrentTenant.operationMode === 1) {
// Skip to subscription step
setActiveTab(2); // index for "SubScription"
setCompletedTabs([0, 1]); // mark previous steps as completed
} else if (HasSelectedCurrentTenant.operationMode === 0) {
// Start from the beginning (new tenant creation)
setActiveTab(0);
}
} else {
// Default: no tenant selected
setActiveTab(0);
}
}, [HasSelectedCurrentTenant,activeTab]);
const tenantForm = useForm({ const tenantForm = useForm({
resolver: zodResolver(newTenantSchema), resolver: zodResolver(newTenantSchema),
@ -35,24 +56,30 @@ const TenantForm = () => {
const handleNext = async () => { const handleNext = async () => {
const currentStepFields = getStepFields(activeTab); const currentStepFields = getStepFields(activeTab);
const trigger = getCurrentTrigger(); const trigger = getCurrentTrigger();
const valid = await trigger(currentStepFields); const valid = await trigger(currentStepFields);
if (valid) { if (valid) {
setCompletedTabs((prev) => [...new Set([...prev, activeTab])]); setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
setActiveTab((prev) => Math.min(prev + 1, newTenantConfig.length - 1));
setActiveTab((prev) => {
let nextStep = Math.min(prev + 1, newTenantConfig.length - 1);
if (HasSelectedCurrentTenant && nextStep === 2) {
nextStep = 3; // skip subscription if already upgraded
}
return nextStep;
});
} }
}; };
const handlePrev = () => { const handlePrev = () => {
setActiveTab((prev) => Math.max(prev - 1, 0)); setActiveTab((prev) => Math.max(prev - 1, 0));
}; };
const onSubmitTenant = (data) => { const onSubmitTenant = (data) => {
console.log(data); // console.log("Tenant Data:", data);
}; };
const onSubmitSubScription = (data) => { const onSubmitSubScription = (data) => {
console.log(data); // console.log("Subscription Data:", data);
}; };
const newTenantConfig = [ const newTenantConfig = [
@ -66,13 +93,24 @@ const TenantForm = () => {
name: "Organization", name: "Organization",
icon: "bx bx-buildings bx-md", icon: "bx bx-buildings bx-md",
subtitle: "Organization Details", subtitle: "Organization Details",
component: <OrganizationInfo onNext={handleNext} onPrev={handlePrev} onSubmitTenant={onSubmitTenant} />, component: (
<OrganizationInfo
onNext={handleNext}
onPrev={handlePrev}
onSubmitTenant={onSubmitTenant}
/>
),
}, },
{ {
name: "SubScription", name: "SubScription",
icon: "bx bx-star bx-md", icon: "bx bx-star bx-md",
subtitle: "Select a plan", subtitle: PlanTextLabel,
component: <SubScription onSubmitSubScription={onSubmitSubScription} onNext={handleNext}/>, component: (
<SubScription
onSubmitSubScription={onSubmitSubScription}
onNext={handleNext}
/>
),
}, },
{ {
name: "congratulation", name: "congratulation",
@ -83,10 +121,13 @@ const TenantForm = () => {
]; ];
const isSubscriptionTab = activeTab === 2; const isSubscriptionTab = activeTab === 2;
return ( return (
<div id="wizard-property-listing" className="bs-stepper horizontically mt-2"> <div id="wizard-property-listing" className="bs-stepper horizontically mt-2">
<div className="bs-stepper-header border-end text-start "> <div className="bs-stepper-header border-end text-start ">
{newTenantConfig.filter((step) => step.name.toLowerCase() !== "congratulation").map((step, index) => { {newTenantConfig
.filter((step) => step.name.toLowerCase() !== "congratulation")
.map((step, index) => {
const isActive = activeTab === index; const isActive = activeTab === index;
const isCompleted = completedTabs.includes(index); const isCompleted = completedTabs.includes(index);
@ -101,7 +142,7 @@ const TenantForm = () => {
<button <button
type="button" type="button"
className={`step-trigger ${isActive ? "active" : ""}`} className={`step-trigger ${isActive ? "active" : ""}`}
onClick={() => setActiveTab(index)} // onClick={() => setActiveTab(index)}
> >
<span className="bs-stepper-circle"> <span className="bs-stepper-circle">
{isCompleted ? ( {isCompleted ? (
@ -112,7 +153,9 @@ const TenantForm = () => {
</span> </span>
<span className="bs-stepper-label"> <span className="bs-stepper-label">
<span className="bs-stepper-title">{step.name}</span> <span className="bs-stepper-title">{step.name}</span>
<span className="bs-stepper-subtitle">{step.subtitle}</span> <span className="bs-stepper-subtitle">
{step.subtitle}
</span>
</span> </span>
</button> </button>
</div> </div>

View File

@ -12,10 +12,16 @@ export const newTenantSchema = z.object({
organizationName: z.string().nonempty("Organization name is required"), organizationName: z.string().nonempty("Organization name is required"),
officeNumber: z.string().nonempty("Office number is required"), officeNumber: z.string().nonempty("Office number is required"),
contactNumber: z.string().nonempty("Contact number is required"), contactNumber: z.string().nonempty("Contact number is required"),
onBoardingDate: z.coerce.date({ onBoardingDate: z.preprocess((val) => {
if (typeof val === "string" && val.includes("-")) {
const [day, month, year] = val.split("-");
return new Date(`${year}-${month}-${day}`);
}
return val;
}, z.date({
required_error: "Onboarding date is required", required_error: "Onboarding date is required",
invalid_type_error: "Invalid date format", invalid_type_error: "Invalid date format",
}), })),
organizationSize: z.string().nonempty("Organization size is required"), organizationSize: z.string().nonempty("Organization size is required"),
industryId: z.string().uuid("Invalid industry ID"), industryId: z.string().uuid("Invalid industry ID"),
reference: z.string().nonempty("Reference is required"), reference: z.string().nonempty("Reference is required"),

View File

@ -90,11 +90,11 @@ export const useCreateTenant = (onSuccessCallback)=>{
}, },
onSuccess:(data,variables)=>{ onSuccess:(data,variables)=>{
showToast("Tenant Created SuccessFully","success") showToast("Tenant Created SuccessFully","success")
dispatch(setCurrentTenant(data)) dispatch(setCurrentTenant({operationMode:0,data:data}))
if(onSuccessCallback) onSuccessCallback() if(onSuccessCallback) onSuccessCallback()
}, },
onError:(error)=>{ onError:(error)=>{
showToast(error.response.message || error.message || `Something went wrong`,"error") showToast(error.response.message || error?.response?.data?.errors || `Something went wrong`,"error")
} }
}) })

View File

@ -7,6 +7,7 @@ import Organization from "../../components/Tenanat/Organization";
import { ComingSoonPage } from "../Misc/ComingSoonPage"; import { ComingSoonPage } from "../Misc/ComingSoonPage";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import EditProfile from "../../components/Tenanat/EditProfile"; import EditProfile from "../../components/Tenanat/EditProfile";
import SubScriptionHistory from "../../components/Tenanat/SubScriptionHistory";
const TenantDetailsContext = createContext(); const TenantDetailsContext = createContext();
export const useTenantDetailsContext = () => useContext(TenantDetailsContext); export const useTenantDetailsContext = () => useContext(TenantDetailsContext);
@ -32,12 +33,12 @@ const TenantDetails = () => {
{ {
id: "navs-left-bill", id: "navs-left-bill",
label: "Bill", label: "Bills and Plan ",
icon: "bx bx-receipt", icon: "bx bx-receipt",
iconSize: "bx-sm", iconSize: "bx-sm",
content: ( content: (
<div className="text-center"> <div className="text-center">
<ComingSoonPage /> <SubScriptionHistory tenantId={tenantId} />
</div> </div>
), ),
}, },

View File

@ -14,6 +14,8 @@ import {
filterSchema, filterSchema,
} from "../../components/Tenanat/TenantSchema"; } from "../../components/Tenanat/TenantSchema";
import TenantFilterPanel from "../../components/Tenanat/TenantFilterPanel"; import TenantFilterPanel from "../../components/Tenanat/TenantFilterPanel";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
// This is context that wrapping all components tenant releated , but must pass inside 'TenantContext.Provider' // This is context that wrapping all components tenant releated , but must pass inside 'TenantContext.Provider'
export const TenantContext = createContext(); export const TenantContext = createContext();
@ -30,6 +32,7 @@ const TenantPage = () => {
const [isRefetching,setRefetching] = useState(false) const [isRefetching,setRefetching] = useState(false)
const [refetchFn, setRefetchFn] = useState(null); const [refetchFn, setRefetchFn] = useState(null);
const [filters, setFilter] = useState(); const [filters, setFilter] = useState();
const dispatch = useDispatch()
const debouncedSearch = useDebounce(searchText, 500); const debouncedSearch = useDebounce(searchText, 500);
const contextValue = { const contextValue = {
}; };
@ -54,7 +57,10 @@ const TenantPage = () => {
setOffcanvasContent("", null); setOffcanvasContent("", null);
}; };
}, []); }, []);
const handleNewTenant =()=>{
dispatch(setCurrentTenant(null))
navigate("/tenants/new-tenant")
}
return ( return (
<TenantContext.Provider value={contextValue}> <TenantContext.Provider value={contextValue}>
<div className="container-fluid"> <div className="container-fluid">
@ -89,8 +95,8 @@ const TenantPage = () => {
data-bs-placement="top" data-bs-placement="top"
data-bs-custom-class="tooltip" data-bs-custom-class="tooltip"
title="Add New Tenant" title="Add New Tenant"
className="p-1 bg-primary rounded-circle" className="p-1 bg-primary rounded-circle cursror-pointer"
onClick={() => navigate("/tenants/new-tenant")} onClick={handleNewTenant}
> >
<i className="bx bx-plus fs-4 text-white"></i> <i className="bx bx-plus fs-4 text-white"></i>
</button> </button>

View File

@ -76,6 +76,13 @@ export const TENANT_STATUS = [
export const CONSTANT_TEXT = { export const CONSTANT_TEXT = {
} }
export const SUBSCRIPTION_PLAN_FREQUENCIES = {
0: "Monthly",
1:"Quatery",
2:"Half-Yearly",
3:"Yearly"
}
export const reference = [ export const reference = [
{ val: "google", name: "Google" }, { val: "google", name: "Google" },
{ val: "frineds", name: "Friends" }, { val: "frineds", name: "Friends" },
@ -90,4 +97,5 @@ export const orgSize = [
]; ];
export const BASE_URL = process.env.VITE_BASE_URL; export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com"; // export const BASE_URL = "https://api.marcoaiot.com";