integrated tenant creation and addsubscription

This commit is contained in:
pramod mahajan 2025-08-07 01:10:59 +05:30
parent 6f2ddacfd1
commit 04580a3f28
11 changed files with 224 additions and 70 deletions

View File

@ -2,10 +2,10 @@ import React, { useState } from "react";
import { useFormContext, Controller } from "react-hook-form"; import { useFormContext, Controller } from "react-hook-form";
import Label from "../common/Label"; import Label from "../common/Label";
import DatePicker from "../common/DatePicker"; import DatePicker from "../common/DatePicker";
import { useIndustries } from "../../hooks/useTenant"; import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
import { LogoUpload } from "./LogoUpload"; import { LogoUpload } from "./LogoUpload";
const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => { const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
const { data, isError, isLoading: industryLoading } = useIndustries(); const { data, isError, isLoading: industryLoading } = useIndustries();
const [logoPreview, setLogoPreview] = useState(null); const [logoPreview, setLogoPreview] = useState(null);
const [logoName, setLogoName] = useState(""); const [logoName, setLogoName] = useState("");
@ -18,6 +18,13 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
formState: { errors }, formState: { errors },
} = useFormContext(); } = useFormContext();
const {
mutate: CreateTenant,
isError: tenantError,
error,
isPending,
} = useCreateTenant(() => onNext());
const handleNext = async () => { const handleNext = async () => {
const valid = await trigger([ const valid = await trigger([
"organizationName", "organizationName",
@ -33,12 +40,13 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
]); ]);
if (valid) { if (valid) {
const data = getValues(); const tenantPayload = getValues();
onSubmitTenant(data); // onSubmitTenant(data);
onNext(); // onNext();
}
};
CreateTenant(tenantPayload);
}
};
const Reference = [ const Reference = [
{ val: "google", name: "Google" }, { val: "google", name: "Google" },
@ -53,7 +61,6 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
{ val: "600", name: "500+" }, { val: "600", name: "500+" },
]; ];
return ( return (
<div className="row g-2"> <div className="row g-2">
<div className="col-sm-6"> <div className="col-sm-6">
@ -167,7 +174,10 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
{...register("organizationSize")} {...register("organizationSize")}
/> */} /> */}
<select className="form-select form-select-sm" {...register("organizationSize")}> <select
className="form-select form-select-sm"
{...register("organizationSize")}
>
{orgSize.map((org) => ( {orgSize.map((org) => (
<option key={org.val} value={org.val}> <option key={org.val} value={org.val}>
{org.name} {org.name}
@ -213,7 +223,10 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
}`} }`}
{...register("reference")} {...register("reference")}
/> */} /> */}
<select className="form-select form-select-sm" {...register("reference")}> <select
className="form-select form-select-sm"
{...register("reference")}
>
{Reference.map((org) => ( {Reference.map((org) => (
<option key={org.val} value={org.val}> <option key={org.val} value={org.val}>
{org.name} {org.name}
@ -240,7 +253,7 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
)} )}
</div> </div>
<div className="col-sm-12"> <div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label> <Label htmlFor="logImage">Logo Image</Label>
<LogoUpload <LogoUpload
@ -249,16 +262,14 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
fileName={logoName} fileName={logoName}
setFileName={setLogoName} setFileName={setLogoName}
/> />
</div>
</div>
<div className="d-flex justify-content-between mt-3"> <div className="d-flex justify-content-between mt-3">
<button <button
type="button" type="button"
className="btn btn-sm btn-secondary" className="btn btn-sm btn-secondary"
onClick={onPrev} onClick={onPrev}
disabled={isPending}
> >
Back Back
</button> </button>
@ -266,8 +277,9 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
type="button" type="button"
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
onClick={handleNext} onClick={handleNext}
disabled={isPending}
> >
Next {isPending ? "Please Wait...":"Submit and Next"}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,14 +1,15 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useSubscriptionPlan } from "../../hooks/useTenant"; import { useAddSubscription, useSubscriptionPlan } from "../../hooks/useTenant";
import SegmentedControl from "./SegmentedControl"; import SegmentedControl from "./SegmentedControl";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { CONSTANT_TEXT } from "../../utils/constants"; import { CONSTANT_TEXT } from "../../utils/constants";
import Label from "../common/Label"; import Label from "../common/Label";
import { useSelector } from "react-redux";
const SubScription = ({ onSubmitSubScription }) => { const SubScription = ({ onSubmitSubScription }) => {
const [frequency, setFrequency] = useState(2); const [frequency, setFrequency] = useState(2);
const [selectedPlanId, setSelectedPlanId] = useState(null); const [selectedPlanId, setSelectedPlanId] = useState(null);
const selectedTenant = useSelector((store)=>store.globalVariables.currentTenant)
const { const {
data: plans = [], data: plans = [],
isError, isError,
@ -23,7 +24,7 @@ const SubScription = ({ onSubmitSubScription }) => {
formState: { errors }, formState: { errors },
} = useFormContext(); } = useFormContext();
const {mutate:AddSubScription,isPending,error} = useAddSubscription()
const handleSubscriptionSubmit = async () => { const handleSubscriptionSubmit = async () => {
const isValid = await trigger([ const isValid = await trigger([
"planId", "planId",
@ -36,7 +37,9 @@ const SubScription = ({ onSubmitSubScription }) => {
if (isValid) { if (isValid) {
const payload = getValues(); const payload = getValues();
onSubmitSubScription(payload); // onSubmitSubScription(payload);
const subscriptionPayload = {...payload,tenantId:selectedTenant.id}
AddSubScription(subscriptionPayload)
} }
}; };
@ -62,7 +65,7 @@ const SubScription = ({ onSubmitSubScription }) => {
<div key={plan.id} className="col-md-4"> <div key={plan.id} className="col-md-4">
<div <div
className={`card h-100 shadow-none border-1 cursor-pointer ${ className={`card h-100 shadow-none border-1 cursor-pointer ${
isSelected ? "border-primary border-2" : "" isSelected ? "border-primary border-1 shadow-md" : ""
}`} }`}
onClick={() => handlePlanSelection(plan)} onClick={() => handlePlanSelection(plan)}
> >
@ -83,11 +86,11 @@ const SubScription = ({ onSubmitSubScription }) => {
<ul className="list-unstyled d-flex gap-4 flex-wrap mb-2"> <ul className="list-unstyled d-flex gap-4 flex-wrap mb-2">
<li className="d-flex align-items-center"> <li className="d-flex align-items-center">
<i className="bx bx-check-double text-success me-1"></i> <i className="bx bx-group me-1"></i>
Max Users {plan.maxUser} Max Users {plan.maxUser}
</li> </li>
<li className="d-flex align-items-center"> <li className="d-flex align-items-center">
<i className="bx bx-check-double text-success me-2"></i> <i className="bx bx-server me-1"></i>
Storage {plan.maxStorage} MB Storage {plan.maxStorage} MB
</li> </li>
<li className="d-flex align-items-center"> <li className="d-flex align-items-center">
@ -110,11 +113,9 @@ const SubScription = ({ onSubmitSubScription }) => {
className="mb-2 d-flex align-items-center" className="mb-2 d-flex align-items-center"
> >
<i <i
className={`bx bx-check bx-xs rounded-circle text-white ${ className={`fa-regular ${mod.enabled ? "fa-circle-check text-success" : "fa-circle-xmark text-danger"} `}
mod.enabled ? "bg-success" : "bg-secondary"
}`}
></i> ></i>
<small className="ms-2">{mod.name}</small> <small className="ms-1">{mod.name}</small>
</div> </div>
))} ))}
</div> </div>
@ -187,10 +188,11 @@ const SubScription = ({ onSubmitSubScription }) => {
<div className="d-flex text-center mt-4"> <div className="d-flex text-center mt-4">
<button <button
onClick={handleSubscriptionSubmit} onClick={handleSubscriptionSubmit}
className="btn btn-primary" className="btn btn-sm btn-primary"
type="button" type="button"
disabled={isPending}
> >
Submit {isPending ? "Please Wait...":"Submit"}
</button> </button>
</div> </div>
</div> </div>

View File

@ -59,7 +59,7 @@ const TenantForm = () => {
}, },
{ {
name: "Organization", name: "Organization",
icon: "bx bx-home bx-md", icon: "bx bx-buildings bx-md",
subtitle: "Organization Details", subtitle: "Organization Details",
component: <OrganizationInfo onNext={handleNext} onSubmitTenant={onSubmitTenant} />, component: <OrganizationInfo onNext={handleNext} onSubmitTenant={onSubmitTenant} />,
}, },

View File

@ -1,6 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useTenants } from "../../hooks/useTenant"; import { useTenants } from "../../hooks/useTenant";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import { getTenantStatus } from "../../utils/dateUtils";
import IconButton from "../common/IconButton";
const TenantsList = () => { const TenantsList = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -21,17 +23,110 @@ const TenantsList = () => {
<h1>Loading...</h1> <h1>Loading...</h1>
</div> </div>
); );
if (isError) return <div>{error}</div>; if (isError) return <div>{error.message}</div>;
const TenantColumns = [
{
key: "name",
label: "Organization",
getValue: (t) => (<div className="d-flex align-items-center py-1">
<IconButton iconClass="bx bx-sm bx-building" color="warning" size={8}/>{t.name || "N/A"}
</div>),
align: "text-start",
},
{
key: "domainName",
label: "Domain",
getValue: (t) =>(
<div style={{width:'160px'}} className="text-truncate">
<a href={t.domainName} className="text-decoration-none">
<i className="bx bx-globe text-primary bx-xs me-2"></i>
{t.domainName || "N/A"}
</a>
</div>
),
align: "text-start",
},
{
key: "contactName",
label: "Contact Person",
getValue: (t) => (<div className="d-flex align-items-center"><i className="bx bx-sm bx-user me-1"/>{t.contactName || "N/A"}</div>),
align: "text-start",
},
{
key: "contactNumber",
label: "Contact",
getValue: (t) => t.contactNumber || "N/A",
isAlwaysVisible: true,
},
{
key: "status",
label: "Status",
align: "text-center",
getValue: (t) => (
<span
className={`badge ${
getTenantStatus(t.tenantStatus?.id) || "secondary"
}`}
>
{t.tenantStatus?.name || "Unknown"}
</span>
),
},
];
return ( return (
<div className="card p-2"> <>
<div className="text-end">
<button className="bt btn-sm btn-primary me-2"> <div className="card p-2 mt-3">
<span class="icon-base bx bx-pie-chart-alt me-1"></span>Create Tenant <div className="card-datatable text-nowrap table-responsive">
</button> <table className="table border-top dataTable text-nowrap">
<thead>
<tr>
{TenantColumns.map((col) => (
<th key={col.key} className="sorting d-table-cell">
<div className={col.align}>{col.label}</div>
</th>
))}
</tr>
</thead>
<tbody>
{data?.data.length > 0 ? (
data.data.map((tenant) => (
<tr key={tenant.id}>
{TenantColumns.map((col) => (
<td
key={col.key}
className={`d-table-cell px-3 py-2 align-middle ${col.align ?? ""}`}
>
{col.customRender
? col.customRender(tenant)
: col.getValue(tenant)}
</td>
))}
</tr>
))
) : (
<tr>
<td
colSpan={TenantColumns.length + 1}
className="text-center py-4"
>
No Tenants Found
</td>
</tr>
)}
</tbody>
</table>
</div> </div>
<div class="card-datatable text-nowrap table-responsive"></div>
</div> </div>
</>
); );
}; };
export default TenantsList; export default TenantsList

View File

@ -15,7 +15,7 @@ const IconButton = ({
iconClass, // icon class string like 'bx bx-user' iconClass, // icon class string like 'bx bx-user'
color = "primary", color = "primary",
onClick, onClick,
size = 20, size = 5,
radius=null, radius=null,
style = {}, style = {},
...rest ...rest
@ -31,7 +31,7 @@ const IconButton = ({
style={{ style={{
backgroundColor, backgroundColor,
color: iconColor, color: iconColor,
padding: "0.4rem", padding: "0.3rem",
margin:'0rem 0.2rem', margin:'0rem 0.2rem',
...style, ...style,
}} }}

View File

@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { TenantRepository } from "../repositories/TenantRepository"; import { TenantRepository } from "../repositories/TenantRepository";
import { MarketRepository } from "../repositories/MarketRepository"; import { MarketRepository } from "../repositories/MarketRepository";
import showToast from "../services/toastService"; import showToast from "../services/toastService";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../slices/globalVariablesSlice";
export const useTenants = ( export const useTenants = (
pageSize, pageSize,
@ -19,7 +21,6 @@ export const useTenants = (
return response.data; return response.data;
}, },
keepPreviousData: true, keepPreviousData: true,
staleTime: 5 * 60 * 1000,
}); });
}; };
@ -46,7 +47,8 @@ export const useSubscriptionPlan=(freq)=>{
// ------------Mutation--------------------- // ------------Mutation---------------------
export const usecreateTenant = (onSuccessCallback)=>{ export const useCreateTenant = (onSuccessCallback)=>{
const dispatch = useDispatch()
return useMutation({ return useMutation({
mutationFn:async(tenantPayload)=>{ mutationFn:async(tenantPayload)=>{
const res = await TenantRepository.createTenant(tenantPayload); const res = await TenantRepository.createTenant(tenantPayload);
@ -54,7 +56,7 @@ export const usecreateTenant = (onSuccessCallback)=>{
}, },
onSuccess:(data,variables)=>{ onSuccess:(data,variables)=>{
showToast("Tenant Created SuccessFully","success") showToast("Tenant Created SuccessFully","success")
dispatch(setCurrentTenant(data))
if(onSuccessCallback) onSuccessCallback() if(onSuccessCallback) onSuccessCallback()
}, },
onError:(error)=>{ onError:(error)=>{
@ -63,3 +65,16 @@ export const usecreateTenant = (onSuccessCallback)=>{
}) })
} }
export const useAddSubscription =(onSuccessCallback)=>{
return useMutation({
mutationFn:async(subscriptionPayload)=>{
const res = await TenantRepository.createTenant(subscriptionPayload);
return res.data;
},
onSuccess:(data,variables)=>{
showToast("Tenant Plan Added SuccessFully","success")
if(onSuccessCallback) onSuccessCallback()
}
})
}

View File

@ -1,8 +1,10 @@
import React from 'react' import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb' import Breadcrumb from '../../components/common/Breadcrumb'
import TenantsList from '../../components/Tenanat/TenantsList' import TenantsList from '../../components/Tenanat/TenantsList'
import { useNavigate } from 'react-router-dom'
const TenantPage = () => { const TenantPage = () => {
const navigate = useNavigate()
return ( return (
<div className='container-fluid'> <div className='container-fluid'>
<Breadcrumb <Breadcrumb
@ -11,6 +13,21 @@ const TenantPage = () => {
{ label: "Tenant", link: null }, { label: "Tenant", link: null },
]} ]}
/> />
<div className="card p-2">
<div className="text-end">
<button
type="button"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Add New Tenant"
className={`p-1 me-2 bg-primary rounded-circle`} onClick={()=>navigate('/tenants/new-tenant')}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
<TenantsList /> <TenantsList />

View File

@ -4,11 +4,13 @@ export const TenantRepository = {
getTenanatList:(pageSize, pageNumber, filter,searchString)=>{ getTenanatList:(pageSize, pageNumber, filter,searchString)=>{
const payloadJsonString = JSON.stringify(filter); const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/Tenant/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`) return api.get(`/api/Tenant/list?pageSize=${pageSize}&pageNumber=${pageNumber}`)
}, },
getSubscriptionPlan:(freq)=>api.get(`/api/Tenant/list/subscription-plan?frequency=${freq}`), getSubscriptionPlan:(freq)=>api.get(`/api/Tenant/list/subscription-plan?frequency=${freq}`),
createTenant:(data)=>api.post('/api/Tenant/create',data) createTenant:(data)=>api.post('/api/Tenant/create',data),
addSubscription:(data)=>api.post('/api/Tenant/create',data)
} }

View File

@ -3,7 +3,8 @@ import { createSlice } from "@reduxjs/toolkit";
const globalVariablesSlice = createSlice({ const globalVariablesSlice = createSlice({
name: "globalVariables", name: "globalVariables",
initialState: { initialState: {
loginUser:null loginUser:null,
currentTenant:null
}, },
reducers: { reducers: {
setGlobalVariable: (state, action) => { setGlobalVariable: (state, action) => {
@ -13,9 +14,12 @@ const globalVariablesSlice = createSlice({
setLoginUserPermmisions: ( state, action ) => setLoginUserPermmisions: ( state, action ) =>
{ {
state.loginUser = action.payload state.loginUser = action.payload
},
setCurrentTenant:(state,action)=>{
state.currentTenant = action.payload
} }
}, },
}); });
export const { setGlobalVariable,setLoginUserPermmisions } = globalVariablesSlice.actions; export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions;
export default globalVariablesSlice.reducer; export default globalVariablesSlice.reducer;

View File

@ -44,9 +44,11 @@ export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"
export const BASIC_PLAN = "08ddd43e-ca62-4cf4-8d7b-569a364307f4" export const BASIC_PLAN = "08ddd43e-ca62-4cf4-8d7b-569a364307f4"
export const BASE_URL = process.env.VITE_BASE_URL; export const BASE_URL = process.env.VITE_BASE_URL;
export const ActiveTenant = "62b05792-5115-4f99-8ff5-e8374859b191"
// export const BASE_URL = "https://api.marcoaiot.com"; // export const BASE_URL = "https://api.marcoaiot.com";
export const CONSTANT_TEXT = { export const CONSTANT_TEXT = {
RenewsubscriptionLabel : " This option; if checked, will renew your productive subscription, if the current plan expires. However, this might prevent you from" RenewsubscriptionLabel : " This option; if checked, will renew your productive subscription, if the current plan expires."
} }

View File

@ -1,4 +1,5 @@
import moment from "moment"; import moment from "moment";
import { ActiveTenant } from "./constants";
export const getDateDifferenceInDays = (startDate, endDate) => { export const getDateDifferenceInDays = (startDate, endDate) => {
if (!startDate || !endDate) { if (!startDate || !endDate) {
@ -79,3 +80,7 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
return clamped.toFixed(2); return clamped.toFixed(2);
} }
export const getTenantStatus =(statusId)=>{
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
}