initially setup tenant manag. setup wizard-form and properly manage data. handle two sperate form with on wizard

This commit is contained in:
pramod mahajan 2025-08-05 02:13:38 +05:30
parent f9e9e2b326
commit 5e28f9e372
12 changed files with 709 additions and 0 deletions

View File

@ -0,0 +1,107 @@
import React from "react";
import Label from "../common/Label";
import { useFormContext } from "react-hook-form";
const ContactInfro = ({ onNext }) => {
const {
register,
control,
trigger,
formState: { errors },
} = useFormContext();
const handleNext = async () => {
const valid = await trigger([
"firstName",
"lastName",
"email",
"contactNumber",
"billingAddress",
]);
if (valid) {
onNext(); // go to next tab
}
};
return (
<div class="row g-6">
<div className="col-sm-6">
<Label htmlFor="firstName" required>
First Name
</Label>
<input
id="firstName"
type="text"
className={`form-control form-control-sm ${errors.firstName ? "is-invalid" : ""}`}
{...register("firstName")}
/>
{errors.firstName && (
<div className="invalid-feedback">{errors.firstName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="lastName" required>
Last Name
</Label>
<input
id="lastName"
type="text"
className={`form-control form-control-sm ${errors.lastName ? "is-invalid" : ""}`}
{...register("lastName")}
/>
{errors.lastName && (
<div className="invalid-feedback">{errors.lastName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="email" required>
Email
</Label>
<input
id="email"
type="email"
className={`form-control form-control-sm ${errors.email ? "is-invalid" : ""}`}
{...register("email")}
/>
{errors.email && (
<div className="invalid-feedback">{errors.email.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
id="contactNumber"
type="text"
className={`form-control form-control-sm ${errors.contactNumber ? "is-invalid" : ""}`}
{...register("contactNumber")}
/>
{errors.contactNumber && (
<div className="invalid-feedback">{errors.contactNumber.message}</div>
)}
</div>
<div className="col-12">
<Label htmlFor="billingAddress" required>
Billing Address
</Label>
<textarea
id="billingAddress"
className={`form-control ${errors.billingAddress ? "is-invalid" : ""}`}
{...register("billingAddress")}
rows={3}
/>
{errors.billingAddress && (
<div className="invalid-feedback">{errors.billingAddress.message}</div>
)}
</div>
<div className="d-flex justify-content-end mt-3">
<button type="button" className="btn btn-primary" onClick={handleNext}>
Next
</button>
</div>
</div>
);
};
export default ContactInfro;

View File

@ -0,0 +1,234 @@
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import Label from "../common/Label";
import DatePicker from "../common/DatePicker";
const OrganizationInfo = ({ onNext, onPrev }) => {
const {
register,
control,
trigger,
formState: { errors },
} = useFormContext();
const handleNext = async () => {
const valid = await trigger([
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
]);
if (valid) onNext();
};
return (
<div className="row g-6">
<div className="col-sm-6">
<Label htmlFor="organizationName" required>
Organization Name
</Label>
<input
id="organizationName"
className={`form-control form-control-sm ${
errors.organizationName ? "is-invalid" : ""
}`}
{...register("organizationName")}
/>
{errors.organizationName && (
<div className="invalid-feedback">
{errors.organizationName.message}
</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="officeNumber" required>
Office Number
</Label>
<input
id="officeNumber"
className={`form-control form-control-sm ${
errors.officeNumber ? "is-invalid" : ""
}`}
{...register("officeNumber")}
/>
{errors.officeNumber && (
<div className="invalid-feedback">{errors.officeNumber.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="domainName" required>
Domain Name
</Label>
<input
id="domainName"
className={`form-control form-control-sm ${
errors.domainName ? "is-invalid" : ""
}`}
{...register("domainName")}
/>
{errors.domainName && (
<div className="invalid-feedback">{errors.domainName.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="taxId" required>
Tax ID
</Label>
<input
id="taxId"
className={`form-control form-control-sm ${
errors.taxId ? "is-invalid" : ""
}`}
{...register("taxId")}
/>
{errors.taxId && (
<div className="invalid-feedback">{errors.taxId.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="onBoardingDate" required>
Onboarding Date
</Label>
{/* this will upcomming from main */}
{/* <DatePicker
name="onBoardingDate"
control={control}
placeholder="Select onboarding date"
maxDate={new Date()}
className={errors.onBoardingDate ? "is-invalid" : ""}
/>
{errors.onBoardingDate && (
<div className="invalid-feedback">{errors.onBoardingDate.message}</div>
)} */}
<label htmlFor="onBoardingDate" className="form-label">
Onboarding Date <span className="text-danger">*</span>
</label>
<input
type="date"
id="onBoardingDate"
{...register("onBoardingDate")}
className={`form-control form-control-sm ${
errors.onBoardingDate ? "is-invalid" : ""
}`}
/>
{errors.onBoardingDate && (
<div className="invalid-feedback">
{errors.onBoardingDate.message}
</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
<input
id="organizationSize"
type="number"
className={`form-control form-control-sm ${
errors.organizationSize ? "is-invalid" : ""
}`}
{...register("organizationSize")}
/>
{errors.organizationSize && (
<div className="invalid-feedback">
{errors.organizationSize.message}
</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="industryId" required>
Industry
</Label>
<Controller
name="industryId"
control={control}
render={({ field }) => (
<Select
{...field}
id="industryId"
className={`form-control form-control-sm ${
errors.industryId ? "is-invalid" : ""
}`}
options={[
{ label: "Tech", value: 1 },
{ label: "Healthcare", value: 2 },
]} // replace with your data
/>
)}
/>
{errors.industryId && (
<div className="invalid-feedback">{errors.industryId.message}</div>
)}
</div>
<div className="col-sm-6">
<Label htmlFor="reference">Reference</Label>
<input
id="reference"
className={`form-control form-control-sm ${
errors.reference ? "is-invalid" : ""
}`}
{...register("reference")}
/>
{errors.reference && (
<div className="invalid-feedback">{errors.reference.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="description">Description</Label>
<textarea
id="description"
rows={3}
className={`form-control form-control-sm ${
errors.description ? "is-invalid" : ""
}`}
{...register("description")}
/>
{errors.description && (
<div className="invalid-feedback">{errors.description.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="logoImage">Logo Image</Label>
<input
id="logoImage"
type="file"
className={`form-control form-control-sm ${
errors.logoImage ? "is-invalid" : ""
}`}
{...register("logoImage")}
/>
{errors.logoImage && (
<div className="invalid-feedback">{errors.logoImage.message}</div>
)}
</div>
<div className="d-flex justify-content-between mt-3">
<button type="button" className="btn btn-secondary" onClick={onPrev}>
Back
</button>
<button type="button" className="btn btn-primary" onClick={handleNext}>
Next
</button>
</div>
</div>
);
};
export default OrganizationInfo;

View File

@ -0,0 +1,9 @@
import React from 'react'
const SubScription = () => {
return (
<div>SubScription</div>
)
}
export default SubScription

View File

@ -0,0 +1,133 @@
import React, { useState } from "react";
import ContactInfro from "./ContactInfro";
import SubScription from "./SubScription";
import OrganizationInfo from "./OrganizationInfo";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
getStepFields,
newTenantSchema,
subscriptionDefaultValues,
subscriptionSchema,
tenantDefaultValues,
} from "./TenantSchema";
const TenantForm = () => {
const [activeTab, setActiveTab] = useState(0);
const [completedTabs, setCompletedTabs] = useState([]);
const tenantForm = useForm({
resolver: zodResolver(newTenantSchema),
defaultValues: tenantDefaultValues,
});
const subscriptionForm = useForm({
resolver: zodResolver(subscriptionSchema),
defaultValues: subscriptionDefaultValues,
});
const getCurrentTrigger = () =>
activeTab === 2 ? subscriptionForm.trigger : tenantForm.trigger;
const handleNext = async () => {
const currentStepFields = getStepFields(activeTab);
const trigger = getCurrentTrigger();
const valid = await trigger(currentStepFields);
if (valid) {
setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
setActiveTab((prev) => Math.min(prev + 1, newTenantConfig.length - 1));
}
};
const onSubmitTenant = (data) => {
console.log(data);
};
const onSubmitSubScription = () => {
console.log(data);
};
const newTenantConfig = [
{
name: "Contact Info",
icon: "bx bx-user bx-md",
subtitle: "Provide Contact Details",
component: <ContactInfro onNext={handleNext} />,
},
{
name: "Organization",
icon: "bx bx-home bx-md",
subtitle: "Organization Details",
component: <OrganizationInfo onNext={handleNext} />,
},
{
name: "SubScription",
icon: "bx bx-star bx-md",
subtitle: "Select a plan",
component: <SubScription />,
},
];
const isSubscriptionTab = activeTab === 2;
return (
<div id="wizard-property-listing" className="bs-stepper vertical mt-2">
<div className="bs-stepper-header border-end text-start">
{newTenantConfig.map((step, index) => {
const isActive = activeTab === index;
const isCompleted = completedTabs.includes(index);
return (
<React.Fragment key={step.name}>
<div
className={`step ${isActive ? "active" : ""} ${
isCompleted ? "crossed" : ""
}`}
data-target={`#step-${index}`}
>
<button
type="button"
className={`step-trigger ${isActive ? "active" : ""}`}
onClick={() => setActiveTab(index)}
>
<span className="bs-stepper-circle">
{isCompleted ? (
<i className="bx bx-check"></i>
) : (
<i className={step.icon}></i>
)}
</span>
<span className="bs-stepper-label">
<span className="bs-stepper-title">{step.name}</span>
<span className="bs-stepper-subtitle">{step.subtitle}</span>
</span>
</button>
</div>
{index < newTenantConfig.length - 1 && (
<div className="line"></div>
)}
</React.Fragment>
);
})}
</div>
<div className="bs-stepper-content">
{isSubscriptionTab ? (
<FormProvider {...subscriptionForm}>
<form onSubmit={subscriptionForm.handleSubmit(onSubmitTenant)}>
{newTenantConfig[activeTab].component}
</form>
</FormProvider>
) : (
<FormProvider {...tenantForm}>
<form onSubmit={tenantForm.handleSubmit(onSubmitSubScription)}>
{newTenantConfig[activeTab].component}
</form>
</FormProvider>
)}
</div>
</div>
);
};
export default TenantForm;

View File

@ -0,0 +1,100 @@
import { z } from "zod";
export const newTenantSchema = z.object({
firstName: z.string().nonempty("First name is required"),
lastName: z.string().nonempty("Last name is required"),
email: z.string().email("Invalid email address"),
description: z.string().nonempty("Description is required"),
domainName: z.string().nonempty("Domain name is required"),
billingAddress: z.string().nonempty("Billing address is required"),
taxId: z.string().nonempty("Tax ID is required"),
logoImage: z.string().nonempty("Logo image is required"),
organizationName: z.string().nonempty("Organization name is required"),
officeNumber: z.string().nonempty("Office number is required"),
contactNumber: z.string().nonempty("Contact number is required"),
onBoardingDate: z.coerce.date({
required_error: "Onboarding date is required",
invalid_type_error: "Invalid date format",
}),
organizationSize: z.string().nonempty("Organization size is required"),
industryId: z.string().uuid("Invalid industry ID"),
reference: z.string().nonempty("Reference is required"),
});
export const tenantDefaultValues = {
firstName: "",
lastName: "",
email: "",
description: "",
domainName: "",
billingAddress: "",
taxId: "",
logoImage: "",
organizationName: "",
officeNumber: "",
contactNumber: "",
onBoardingDate: new Date(), // or `null` if you want it empty
organizationSize: "",
industryId: "", // should be a valid UUID if pre-filled
reference: "",
};
export const subscriptionSchema = z.object({
tenantId: z.string().uuid("Invalid tenant ID"),
planId: z.string().uuid("Invalid plan ID"),
currencyId: z.string().uuid("Invalid currency ID"),
maxUsers: z
.number({ invalid_type_error: "Max users must be a number" })
.min(1, "At least one user is required"),
frequency: z
.number({ invalid_type_error: "Frequency must be a number" })
.min(1, "Frequency must be at least 1"),
isTrial: z.boolean(),
autoRenew: z.boolean(),
});
export const subscriptionDefaultValues = {
tenantId: "", // should be a UUID
planId: "", // should be a UUID
currencyId: "", // should be a UUID
maxUsers: 1,
frequency: 1,
isTrial: false,
autoRenew: false,
};
export const getStepFields = (stepIndex) => {
const stepFieldMap = {
0: [
"firstName",
"lastName",
"email",
"contactNumber",
"billingAddress",
],
1: [
"organizationName",
"officeNumber",
"domainName",
"description",
"onBoardingDate",
"organizationSize",
"taxId",
"industryId",
"reference",
"logoImage",
],
2: [
"tenantId",
"planId",
"currencyId",
"maxUsers",
"frequency",
"isTrial",
"autoRenew",
],
};
return stepFieldMap[stepIndex] || [];
};

View File

@ -0,0 +1,37 @@
import React, { useState } from "react";
import { useTenants } from "../../hooks/useTenant";
import { ITEMS_PER_PAGE } from "../../utils/constants";
const TenantsList = () => {
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading, isError, isInitialLoading, error } = useTenants(
ITEMS_PER_PAGE,
currentPage
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isInitialLoading)
return (
<div>
<h1>Loading...</h1>
</div>
);
if (isError) return <div>{error}</div>;
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>
<div class="card-datatable text-nowrap table-responsive"></div>
</div>
);
};
export default TenantsList;

View File

@ -0,0 +1,12 @@
import React from "react";
const Label = ({ htmlFor, children, required = false, className = "" }) => {
return (
<label htmlFor={htmlFor} className={`form-label d-block ${className}`}>
{children}
{required && <span className="text-danger ms-1">*</span>}
</label>
);
};
export default Label;

22
src/hooks/useTenant.js Normal file
View File

@ -0,0 +1,22 @@
import { useQuery } from "@tanstack/react-query";
import { TenantRepository } from "../repositories/TenantRepository";
export const useTenants = (
pageSize,
pageNumber,
filter,
searchString = ""
) => {
return useQuery({
queryKey: ["Tenants", pageNumber, pageSize],
queryFn: async () => {
const response = await TenantRepository.getTenanatList(
pageSize,
pageNumber,
);
return response.data;
},
keepPreviousData: true,
staleTime: 5 * 60 * 1000,
});
};

View File

@ -0,0 +1,20 @@
import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb'
import TenantForm from '../../components/Tenanat/TenantForm'
const CreateTenant = () => {
return (
<div className='container-fluid'>
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: '/tenants' },
{ label: "New Tenant", link: null },
]}
/>
<TenantForm/>
</div>
)
}
export default CreateTenant

View File

@ -0,0 +1,21 @@
import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb'
import TenantsList from '../../components/Tenanat/TenantsList'
const TenantPage = () => {
return (
<div className='container-fluid'>
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: null },
]}
/>
<TenantsList />
</div>
)
}
export default TenantPage

View File

@ -0,0 +1,10 @@
import { api } from "../utils/axiosClient";
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}`)
}
}

View File

@ -39,6 +39,8 @@ import ProtectedRoute from "./ProtectedRoute";
import Directory from "../pages/Directory/Directory";
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
import ExpensePage from "../pages/Expense/ExpensePage";
import TenantPage from "../pages/Tenant/TenantPage";
import CreateTenant from "../pages/Tenant/CreateTenant";
const router = createBrowserRouter(
[
@ -79,6 +81,8 @@ const router = createBrowserRouter(
{ path: "/gallary", element: <ImageGallary /> },
{ path: "/expenses", element: <ExpensePage /> },
{ path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/help/support", element: <Support /> },
{ path: "/help/docs", element: <Documentation /> },
{ path: "/help/connect", element: <Connect /> },