integrated tenant creation and addsubscription

This commit is contained in:
pramod mahajan 2025-08-07 01:10:59 +05:30
parent 34b429c28c
commit fbc53e9ea1
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 Label from "../common/Label";
import DatePicker from "../common/DatePicker";
import { useIndustries } from "../../hooks/useTenant";
import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
import { LogoUpload } from "./LogoUpload";
const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
const { data, isError, isLoading: industryLoading } = useIndustries();
const [logoPreview, setLogoPreview] = useState(null);
const [logoName, setLogoName] = useState("");
@ -18,27 +18,35 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
formState: { errors },
} = useFormContext();
const {
mutate: CreateTenant,
isError: tenantError,
error,
isPending,
} = useCreateTenant(() => onNext());
const handleNext = async () => {
const valid = await trigger([
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
]);
const valid = await trigger([
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
]);
if (valid) {
const data = getValues();
onSubmitTenant(data);
onNext();
}
};
if (valid) {
const tenantPayload = getValues();
// onSubmitTenant(data);
// onNext();
CreateTenant(tenantPayload);
}
};
const Reference = [
{ val: "google", name: "Google" },
@ -53,7 +61,6 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
{ val: "600", name: "500+" },
];
return (
<div className="row g-2">
<div className="col-sm-6">
@ -167,7 +174,10 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
{...register("organizationSize")}
/> */}
<select className="form-select form-select-sm" {...register("organizationSize")}>
<select
className="form-select form-select-sm"
{...register("organizationSize")}
>
{orgSize.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
@ -213,7 +223,10 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
}`}
{...register("reference")}
/> */}
<select className="form-select form-select-sm" {...register("reference")}>
<select
className="form-select form-select-sm"
{...register("reference")}
>
{Reference.map((org) => (
<option key={org.val} value={org.val}>
{org.name}
@ -240,25 +253,23 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
)}
</div>
<div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
setPreview={setLogoPreview}
fileName={logoName}
setFileName={setLogoName}
/>
</div>
<div className="col-sm-12">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
setPreview={setLogoPreview}
fileName={logoName}
setFileName={setLogoName}
/>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={onPrev}
disabled={isPending}
>
Back
</button>
@ -266,8 +277,9 @@ const OrganizationInfo = ({ onNext, onPrev,onSubmitTenant }) => {
type="button"
className="btn btn-sm btn-primary"
onClick={handleNext}
disabled={isPending}
>
Next
{isPending ? "Please Wait...":"Submit and Next"}
</button>
</div>
</div>

View File

@ -1,14 +1,15 @@
import React, { useState, useEffect } from "react";
import { useSubscriptionPlan } from "../../hooks/useTenant";
import { useAddSubscription, useSubscriptionPlan } from "../../hooks/useTenant";
import SegmentedControl from "./SegmentedControl";
import { useFormContext } from "react-hook-form";
import { CONSTANT_TEXT } from "../../utils/constants";
import Label from "../common/Label";
import { useSelector } from "react-redux";
const SubScription = ({ onSubmitSubScription }) => {
const [frequency, setFrequency] = useState(2);
const [selectedPlanId, setSelectedPlanId] = useState(null);
const selectedTenant = useSelector((store)=>store.globalVariables.currentTenant)
const {
data: plans = [],
isError,
@ -23,7 +24,7 @@ const SubScription = ({ onSubmitSubScription }) => {
formState: { errors },
} = useFormContext();
const {mutate:AddSubScription,isPending,error} = useAddSubscription()
const handleSubscriptionSubmit = async () => {
const isValid = await trigger([
"planId",
@ -36,7 +37,9 @@ const SubScription = ({ onSubmitSubScription }) => {
if (isValid) {
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
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)}
>
@ -83,11 +86,11 @@ const SubScription = ({ onSubmitSubScription }) => {
<ul className="list-unstyled d-flex gap-4 flex-wrap mb-2">
<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}
</li>
<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
</li>
<li className="d-flex align-items-center">
@ -110,11 +113,9 @@ const SubScription = ({ onSubmitSubScription }) => {
className="mb-2 d-flex align-items-center"
>
<i
className={`bx bx-check bx-xs rounded-circle text-white ${
mod.enabled ? "bg-success" : "bg-secondary"
}`}
className={`fa-regular ${mod.enabled ? "fa-circle-check text-success" : "fa-circle-xmark text-danger"} `}
></i>
<small className="ms-2">{mod.name}</small>
<small className="ms-1">{mod.name}</small>
</div>
))}
</div>
@ -187,10 +188,11 @@ const SubScription = ({ onSubmitSubScription }) => {
<div className="d-flex text-center mt-4">
<button
onClick={handleSubscriptionSubmit}
className="btn btn-primary"
className="btn btn-sm btn-primary"
type="button"
disabled={isPending}
>
Submit
{isPending ? "Please Wait...":"Submit"}
</button>
</div>
</div>

View File

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

View File

@ -1,6 +1,8 @@
import React, { useState } from "react";
import { useTenants } from "../../hooks/useTenant";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { getTenantStatus } from "../../utils/dateUtils";
import IconButton from "../common/IconButton";
const TenantsList = () => {
const [currentPage, setCurrentPage] = useState(1);
@ -21,17 +23,110 @@ const TenantsList = () => {
<h1>Loading...</h1>
</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 (
<div className="card p-2">
<div className="text-end">
<button className="bt btn-sm btn-primary me-2">
<span class="icon-base bx bx-pie-chart-alt me-1"></span>Create Tenant
</button>
<>
<div className="card p-2 mt-3">
<div className="card-datatable text-nowrap table-responsive">
<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 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'
color = "primary",
onClick,
size = 20,
size = 5,
radius=null,
style = {},
...rest
@ -31,7 +31,7 @@ const IconButton = ({
style={{
backgroundColor,
color: iconColor,
padding: "0.4rem",
padding: "0.3rem",
margin:'0rem 0.2rem',
...style,
}}

View File

@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { TenantRepository } from "../repositories/TenantRepository";
import { MarketRepository } from "../repositories/MarketRepository";
import showToast from "../services/toastService";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../slices/globalVariablesSlice";
export const useTenants = (
pageSize,
@ -19,7 +21,6 @@ export const useTenants = (
return response.data;
},
keepPreviousData: true,
staleTime: 5 * 60 * 1000,
});
};
@ -46,7 +47,8 @@ export const useSubscriptionPlan=(freq)=>{
// ------------Mutation---------------------
export const usecreateTenant = (onSuccessCallback)=>{
export const useCreateTenant = (onSuccessCallback)=>{
const dispatch = useDispatch()
return useMutation({
mutationFn:async(tenantPayload)=>{
const res = await TenantRepository.createTenant(tenantPayload);
@ -54,12 +56,25 @@ export const usecreateTenant = (onSuccessCallback)=>{
},
onSuccess:(data,variables)=>{
showToast("Tenant Created SuccessFully","success")
if(onSuccessCallback) onSuccessCallback()
dispatch(setCurrentTenant(data))
if(onSuccessCallback) onSuccessCallback()
},
onError:(error)=>{
showToast(error.response.message || error.message || `Something went wrong`,"error")
}
})
}
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 Breadcrumb from '../../components/common/Breadcrumb'
import TenantsList from '../../components/Tenanat/TenantsList'
import { useNavigate } from 'react-router-dom'
const TenantPage = () => {
const navigate = useNavigate()
return (
<div className='container-fluid'>
<Breadcrumb
@ -11,6 +13,21 @@ const TenantPage = () => {
{ 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 />

View File

@ -4,11 +4,13 @@ export const TenantRepository = {
getTenanatList:(pageSize, pageNumber, filter,searchString)=>{
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}`),
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({
name: "globalVariables",
initialState: {
loginUser:null
loginUser:null,
currentTenant:null
},
reducers: {
setGlobalVariable: (state, action) => {
@ -13,9 +14,12 @@ const globalVariablesSlice = createSlice({
setLoginUserPermmisions: ( state, action ) =>
{
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;

View File

@ -68,9 +68,11 @@ export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7"
export const BASIC_PLAN = "08ddd43e-ca62-4cf4-8d7b-569a364307f4"
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 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 { ActiveTenant } from "./constants";
export const getDateDifferenceInDays = (startDate, endDate) => {
if (!startDate || !endDate) {
@ -82,4 +83,8 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> {
const clamped = Math.min(Math.max(percentage, 0), 100);
return clamped.toFixed(2);
}
export const getTenantStatus =(statusId)=>{
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
}