diff --git a/src/components/Tenant/CreateTenant.jsx b/src/components/Tenant/CreateTenant.jsx index 0391642e..a78a7332 100644 --- a/src/components/Tenant/CreateTenant.jsx +++ b/src/components/Tenant/CreateTenant.jsx @@ -5,6 +5,7 @@ import Breadcrumb from "../common/Breadcrumb"; import { apiTenant } from "./apiTenant"; import { useCreateTenant } from "./useTenants"; import TenantSubscription from "./TenantSubscription"; +import { useQueryClient } from "@tanstack/react-query"; const defaultAvatar = "https://via.placeholder.com/100x100.png?text=Avatar"; const initialData = { @@ -22,18 +23,28 @@ const initialData = { taxId: "", billingAddress: "", onBoardingDate: "", + status: "", }; +// RequiredLabel component remains the same for mandatory fields const RequiredLabel = ({ label, name }) => ( ); +// Regular label for non-mandatory fields +const RegularLabel = ({ label, name }) => ( + +); + const validateForm = (form, step) => { let errors = {}; let fieldsToValidate = []; + // Updated list of mandatory fields for each step if (step === 1) { fieldsToValidate = [ "firstName", @@ -45,11 +56,11 @@ const validateForm = (form, step) => { } else if (step === 2) { fieldsToValidate = [ "organizationName", + "onBoardingDate", "organizationSize", "industryId", "reference", - "domainName", - "onBoardingDate", + "status", ]; } @@ -71,7 +82,9 @@ const CreateTenant = () => { const navigate = useNavigate(); const { state } = useLocation(); const formData = state?.formData || null; - const { createTenant, updateTenant, loading } = useCreateTenant(); + // const { createTenant, updateTenant, loading } = useCreateTenant(); + const { mutate: createTenant, isPending } = useCreateTenant(); + const queryClient = useQueryClient(); const [form, setForm] = useState(initialData); const [formErrors, setFormErrors] = useState({}); @@ -169,7 +182,7 @@ const CreateTenant = () => { } setFormErrors({}); const finalLogoImage = imageFile || (imagePreview === defaultAvatar ? null : imagePreview); - const submissionData = { + const tenantPayload = { ...form, logoImage: finalLogoImage, contactNumber: form.phone, @@ -177,14 +190,15 @@ const CreateTenant = () => { }; let result; if (formData?.id) { - result = await updateTenant(formData.id, submissionData); + // result = await updateTenant(formData.id, tenantPayload); if (result) navigate("/tenant/profile", { state: { newTenant: result } }); } else { - result = await createTenant(submissionData); - if (result) navigate("/tenant/profile/viewtenant", { state: { formData: result } }); + // result = await createTenant(submissionData); + // if (result) navigate("/tenant/profile/viewtenant", { state: { formData: result } }); + createTenant(tenantPayload); } }, - [form, imagePreview, imageFile, formData, navigate, createTenant, updateTenant] + [form, imagePreview, imageFile, formData, navigate, createTenant] ); const renderFormStep = () => { @@ -208,7 +222,7 @@ const CreateTenant = () => { {formErrors?.email && (

{formErrors.email}

)}
- + {formErrors?.phone && (

{formErrors.phone}

)}
@@ -230,7 +244,7 @@ const CreateTenant = () => {
- {formErrors?.organizationName && (

{form.organizationName}

)} + {formErrors?.organizationName && (

{formErrors.organizationName}

)}
@@ -263,21 +277,34 @@ const CreateTenant = () => { {formErrors?.reference && (

{formErrors.reference}

)}
- - + + + {formErrors?.status && (

{formErrors.status}

)}
- - + + + {formErrors?.taxId && (

{formErrors.taxId}

)} +
+
+ + {formErrors?.domainName && (

{formErrors.domainName}

)}
- + + {formErrors?.mobile && (

{formErrors.mobile}

)}
+
- + + {formErrors?.description && (

{formErrors.description}

)}
@@ -354,8 +381,8 @@ const CreateTenant = () => {
{renderFormStep()}
{step > 1 && ()} - {step < 3 && ()} - {step === 3 && ()} + {step < 3 && ()} + {step === 3 && ()}
diff --git a/src/components/Tenant/Tenant.jsx b/src/components/Tenant/Tenant.jsx index 944f2166..658586c0 100644 --- a/src/components/Tenant/Tenant.jsx +++ b/src/components/Tenant/Tenant.jsx @@ -3,6 +3,7 @@ import { useNavigate, useLocation } from "react-router-dom"; import Breadcrumb from "../common/Breadcrumb"; import { useTenants } from "./useTenants"; import Avatar from "../common/Avatar"; +import TenantSkeleton from "./TenantSkeleton"; const Tenant = () => { const { tenants, loading, error } = useTenants(); @@ -65,7 +66,7 @@ const Tenant = () => { }); if (loading) { - return
Loading tenants...
; + return ; } if (error) { @@ -90,19 +91,14 @@ const Tenant = () => {
-
+
- {/* */} - {/* */} - + @@ -147,22 +143,7 @@ const Tenant = () => { {tenant.contactName} - - {/* */} - {/* */} - {/* */} - - - ); })} diff --git a/src/components/Tenant/TenantSkeleton.jsx b/src/components/Tenant/TenantSkeleton.jsx new file mode 100644 index 00000000..9440a229 --- /dev/null +++ b/src/components/Tenant/TenantSkeleton.jsx @@ -0,0 +1,59 @@ +import React from "react"; + +const SkeletonRow = () => ( + + {Array.from({ length: 5 }).map((_, idx) => ( + + ))} + + +); + +const TenantSkeleton = ({ rows = 6 }) => { + return ( +
+
+
+
+ +
+
+ +
+
+ +
+
Organization NameEmailPhoneDomainSizeUser Industry Actions
{tenant.email}{tenant.contactNumber}{tenant.domainName} - {tenant.domainName ? ( - <> - - {tenant.domainName} - - ) : ( - -- - )} - {tenant.organizationSize} {tenant.industry?.name} @@ -172,7 +153,7 @@ const Tenant = () => { onClick={() => handleView(tenant)} > handleEdit(tenant)} > { >
+
+ +
+
+
+ {Array.from({ length: 3 }).map((_, idx) => ( + + ))} +
+
+ + + + + + + + + + + + {Array.from({ length: rows }).map((_, idx) => ( + + ))} + +
OrganizationNamePhoneSizeIndustryActions
+
+
+ + ); +}; + +export default TenantSkeleton; diff --git a/src/components/Tenant/TenantSubscription.jsx b/src/components/Tenant/TenantSubscription.jsx index 048c8463..c95f37ec 100644 --- a/src/components/Tenant/TenantSubscription.jsx +++ b/src/components/Tenant/TenantSubscription.jsx @@ -1,247 +1,31 @@ import React, { useState } from "react"; -// Assuming Breadcrumb is a custom component and pricingData is defined globally or imported -// For this example, we'll define pricingData and a mock Breadcrumb component -const Breadcrumb = () => ( - -); +// import { useSubscriptionPlans } from "./useTenant"; // adjust the path if needed +import { useSubscriptionPlans } from "./useTenants"; +import { useSelector } from "react-redux"; + +const billingOptions = [ + { label: "Monthly", frequency: 0 }, + { label: "Quarterly", frequency: 1 }, + { label: "Half-Yearly", frequency: 2 }, + { label: "Yearly", frequency: 3 }, +]; -const pricingData = { - "Monthly": [ - { - name: "Basic", - price: "$0", - per: "/month", - description: "A simple start for everyone", - features: [ - "100 responses a month", - "Unlimited forms and surveys", - "Unlimited fields", - "Basic form creation tools", - "Up to 2 subdomains", - ], - button: "Your Current Plan", - buttonClass: "btn btn-success disabled", - highlight: false, - }, - { - name: "Standard", - price: "$45", - per: "/month", - description: "For small to medium businesses", - subText: "Billed monthly", - features: [ - "Unlimited responses", - "Unlimited forms and surveys", - "Instagram profile page", - "Google Docs integration", - 'Custom "Thank you" page', - ], - button: "Upgrade", - buttonClass: "btn btn-primary", - highlight: true, - badge: "Popular", - }, - { - name: "Enterprise", - price: "$90", - per: "/month", - description: "Solution for big organizations", - subText: "Billed monthly", - features: [ - "PayPal payments", - "Logic Jumps", - "File upload with 5GB storage", - "Custom domain support", - "Stripe integration", - ], - button: "Buy Now", - buttonClass: "btn btn-warning text-white fw-semibold shadow", - highlight: false, - }, - ], - "Quarterly": [ - { - name: "Basic", - price: "$0", - per: "/quarter", - description: "A simple start for everyone", - features: [ - "100 responses a month", - "Unlimited forms and surveys", - "Unlimited fields", - "Basic form creation tools", - "Up to 2 subdomains", - ], - button: "Your Current Plan", - buttonClass: "btn btn-success disabled", - highlight: false, - }, - { - name: "Standard", - price: "$120", - per: "/quarter", - description: "For small to medium businesses", - subText: "Billed quarterly", - features: [ - "Unlimited responses", - "Unlimited forms and surveys", - "Instagram profile page", - "Google Docs integration", - 'Custom "Thank you" page', - ], - button: "Upgrade", - buttonClass: "btn btn-primary", - highlight: true, - badge: "Popular", - }, - { - name: "Enterprise", - price: "$240", - per: "/quarter", - description: "Solution for big organizations", - subText: "Billed quarterly", - features: [ - "PayPal payments", - "Logic Jumps", - "File upload with 5GB storage", - "Custom domain support", - "Stripe integration", - ], - button: "Buy Now", - buttonClass: "btn btn-warning text-white fw-semibold shadow", - highlight: false, - }, - ], - "Half-Yearly": [ - { - name: "Basic", - price: "$0", - per: "/6 months", - description: "A simple start for everyone", - features: [ - "100 responses a month", - "Unlimited forms and surveys", - "Unlimited fields", - "Basic form creation tools", - "Up to 2 subdomains", - ], - button: "Your Current Plan", - buttonClass: "btn btn-success disabled", - highlight: false, - }, - { - name: "Standard", - price: "$220", - per: "/6 months", - description: "For small to medium businesses", - subText: "USD 220 every 6 months", - features: [ - "Unlimited responses", - "Unlimited forms and surveys", - "Instagram profile page", - "Google Docs integration", - 'Custom "Thank you" page', - ], - button: "Upgrade", - buttonClass: "btn btn-primary", - highlight: true, - badge: "Popular", - }, - { - name: "Enterprise", - price: "$440", - per: "/6 months", - description: "Solution for big organizations", - subText: "USD 440 every 6 months", - features: [ - "PayPal payments", - "Logic Jumps", - "File upload with 5GB storage", - "Custom domain support", - "Stripe integration", - ], - button: "Buy Now", - buttonClass: "btn btn-warning text-white fw-semibold shadow", - highlight: false, - }, - ], - "Yearly": [ - { - name: "Basic", - price: "$0", - per: "/year", - description: "A simple start for everyone", - features: [ - "100 responses a month", - "Unlimited forms and surveys", - "Unlimited fields", - "Basic form creation tools", - "Up to 2 subdomains", - ], - button: "Your Current Plan", - buttonClass: "btn btn-success disabled", - highlight: false, - }, - { - name: "Standard", - price: "$400", - per: "/year", - description: "For small to medium businesses", - subText: "USD 400/year", - features: [ - "Unlimited responses", - "Unlimited forms and surveys", - "Instagram profile page", - "Google Docs integration", - 'Custom "Thank you" page', - ], - button: "Upgrade", - buttonClass: "btn btn-primary", - highlight: true, - badge: "Popular", - }, - { - name: "Enterprise", - price: "$800", - per: "/year", - description: "Solution for big organizations", - subText: "USD 800/year", - features: [ - "PayPal payments", - "Logic Jumps", - "File upload with 5GB storage", - "Custom domain support", - "Stripe integration", - ], - button: "Buy Now", - buttonClass: "btn btn-warning text-white fw-semibold shadow", - highlight: false, - }, - ], -}; -const billingOptions = ["Monthly", "Quarterly", "Half-Yearly", "Yearly"]; const organizationSizes = ["1-10", "11-50", "51-200", "201-500", "501+"]; const TenantSubscription = () => { const [selectedBillingIndex, setSelectedBillingIndex] = useState(null); const [selectedPlanIndex, setSelectedPlanIndex] = useState(null); const [selectedOrgSize, setSelectedOrgSize] = useState(""); + const currentTenant = useSelector((state)=> state.globalVariables.currentTenant); + console.log("karti",currentTenant) - const selectedBillingKey = billingOptions[selectedBillingIndex]; - const plansForBilling = pricingData[selectedBillingKey]; - - const currentPlan = plansForBilling ? plansForBilling[selectedPlanIndex] : null; + const selectedBilling = billingOptions[selectedBillingIndex]; + const { plans, loading, error } = useSubscriptionPlans(selectedBilling?.frequency); + const currentPlan = plans?.[selectedPlanIndex] || null; return (
- - + {/* Breadcrumb placeholder */}

Choose your billing cycle:

@@ -253,102 +37,98 @@ const TenantSubscription = () => { selectedBillingIndex === index ? "btn-primary text-white" : "btn-light" }`} style={{ - borderRight: - index !== billingOptions.length - 1 ? "1px solid #dee2e6" : "none", + borderRight: index !== billingOptions.length - 1 ? "1px solid #dee2e6" : "none", fontWeight: selectedBillingIndex === index ? "600" : "normal", }} onClick={() => { setSelectedBillingIndex(index); - setSelectedPlanIndex(null); + setSelectedPlanIndex(null); }} > - {option} + {option.label} ))}
- - {plansForBilling && ( + + {loading &&

Loading plans...

} + {error &&

{error}

} + + {plans.length > 0 && (

Select a plan:

- {plansForBilling.map((plan, index) => ( + {plans.map((plan, index) => ( ))}
)} -
- {currentPlan && ( + {currentPlan && ( +
-
+
- {currentPlan.badge && ( - - {currentPlan.badge} - - )}
- +
-
{currentPlan.name}
+
{currentPlan.planName}

{currentPlan.description}

+ {currentPlan.currency?.symbol} {currentPlan.price}{" "} - {currentPlan.per} + /{selectedBilling?.label.toLowerCase()}

- {currentPlan.subText &&

{currentPlan.subText}

}
    - {currentPlan.features.map((feat, i) => ( -
  • - - {feat} -
  • - ))} +
  • + + Max Users: {currentPlan.maxUser} +
  • +
  • + + Storage: {currentPlan.maxStorage} MB +
  • +
  • + + Trial Days: {currentPlan.trialDays} +
- +
- )} -
- - {/* Moved the Organization Size dropdown to appear after the details section */} +
+ )} +
- - setSelectedOrgSize(e.target.value)} > {organizationSizes.map((size, index) => ( - + ))}
@@ -356,4 +136,4 @@ const TenantSubscription = () => { ); }; -export default TenantSubscription; \ No newline at end of file +export default TenantSubscription; diff --git a/src/components/Tenant/apiTenant.jsx b/src/components/Tenant/apiTenant.jsx index 130388bd..b2a1b63a 100644 --- a/src/components/Tenant/apiTenant.jsx +++ b/src/components/Tenant/apiTenant.jsx @@ -8,4 +8,8 @@ export const apiTenant = { createTenant: (data) => api.post("api/tenant/create", data), + getSubscriptionPlansTenant: (frequency) => api.get(`api/tenant/list/subscription-plan?frequency=${frequency}`), + + createSubscriptionTenant: (data) => api.post("api/tenant/add-subscription", data), + }; diff --git a/src/components/Tenant/useTenants.js b/src/components/Tenant/useTenants.js index 2b9815aa..547edcf9 100644 --- a/src/components/Tenant/useTenants.js +++ b/src/components/Tenant/useTenants.js @@ -1,5 +1,10 @@ import { useCallback, useEffect, useState } from "react"; import { apiTenant } from "./apiTenant"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useDispatch } from "react-redux"; +import { setCurrentTenant } from "../../slices/globalVariablesSlice"; +import showToast from "../../services/toastService"; +import { useNavigate } from "react-router-dom"; export const useTenants = (page = 1, pageSize = 20) => { const [tenants, setTenants] = useState([]); @@ -28,33 +33,106 @@ export const useTenants = (page = 1, pageSize = 20) => { }; -export const useCreateTenant = () => { - const [loading, setLoading] = useState(false); - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); +// export const useCreateTenant = () => { +// const [loading, setLoading] = useState(false); +// const [data, setData] = useState(null); +// const [error, setError] = useState(null); +// const [success, setSuccess] = useState(false); - const createTenant = useCallback(async (tenantData) => { - setLoading(true); - setError(null); - setData(null); - setSuccess(false); +// const createTenant = useCallback(async (tenantData) => { +// setLoading(true); +// setError(null); +// setData(null); +// setSuccess(false); - try { - const res = await apiTenant.createTenant(tenantData); - setData(res.data); - setSuccess(true); - console.log("Tenant created successfully:", res.data); +// try { +// const res = await apiTenant.createTenant(tenantData); +// setData(res.data); +// setSuccess(true); +// console.log("Tenant created successfully:", res.data); +// return res.data; +// } catch (err) { +// console.error("Failed to create tenant:", err); +// setError(err); +// setSuccess(false); +// return null; +// } finally { +// setLoading(false); +// } +// }, []); + +// return { createTenant, loading, data, error, success }; +// }; + + +export const useCreateTenant = ()=>{ + const queryClient = useQueryClient(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + return useMutation({ + mutationFn: async (tenantPayload) => { + const res = await apiTenant.createTenant(tenantPayload); return res.data; - } catch (err) { - console.error("Failed to create tenant:", err); - setError(err); - setSuccess(false); - return null; - } finally { - setLoading(false); - } - }, []); + }, + onSuccess:(data,variables)=>{ + dispatch(setCurrentTenant(data)); + showToast("Tenant created successfully","sucess"); + navigate("/tenant/profile/viewtenant", { state: { formData: result } }); + }, + onError: (error) => { + showToast(error.message || "Failed to mark Tenant", "error"); + }, + }) +} + +export const createSubscriptionTenant = ()=>{ + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (subscriptionPayload) => { + const res = await apiTenant.createSubscriptionTenant(subscriptionPayload); + return res.data; + }, + onSuccess:(data,variables)=>{ + dispatch(setCurrentTenant(data)); + showToast("Tenant created successfully","sucess"); + }, + onError: (error) => { + showToast(error.message || "Failed to mark Tenant", "error"); + }, + }) +} + +export const useSubscriptionPlans = (frequency) => { + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!frequency) return; + + const fetchPlans = async () => { + setLoading(true); + setError(null); + + try { + const response = await apiTenant.getSubscriptionPlansTenant(frequency); + if (response?.data) { + setPlans(response.data); + } else { + setError("Failed to fetch plans."); + } + } catch (err) { + setError(err.message || "Something went wrong."); + } finally { + setLoading(false); + } + }; + + fetchPlans(); + }, [frequency]); + + return { plans, loading, error }; +}; - return { createTenant, loading, data, error, success }; -}; \ No newline at end of file diff --git a/src/hooks/useAttendance.js b/src/hooks/useAttendance.js index 05937aa6..8badbba5 100644 --- a/src/hooks/useAttendance.js +++ b/src/hooks/useAttendance.js @@ -286,6 +286,7 @@ export const useMarkAttendance = () => { if(variables.forWhichTab !== 3) showToast("Attendance marked successfully", "success"); }, + onError: (error) => { showToast(error.message || "Failed to mark attendance", "error"); }, diff --git a/src/slices/globalVariablesSlice.jsx b/src/slices/globalVariablesSlice.jsx index 4e14191d..fb985b42 100644 --- a/src/slices/globalVariablesSlice.jsx +++ b/src/slices/globalVariablesSlice.jsx @@ -1,9 +1,10 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createSlice, current } from "@reduxjs/toolkit"; const globalVariablesSlice = createSlice({ name: "globalVariables", initialState: { - loginUser:null + loginUser:null, + currentTenant:null }, reducers: { setGlobalVariable: (state, action) => { @@ -13,9 +14,14 @@ const globalVariablesSlice = createSlice({ setLoginUserPermmisions: ( state, action ) => { state.loginUser = action.payload + }, + setCurrentTenant:(state, action)=> + { + state.currentTenant = action.payload; + localStorage.setItem("currentTenant",state.currentTenant.id); } }, }); -export const { setGlobalVariable,setLoginUserPermmisions } = globalVariablesSlice.actions; +export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions; export default globalVariablesSlice.reducer;