Design changes in Create Tenant form.
This commit is contained in:
parent
de854e87f3
commit
6c3f64bf24
@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import Breadcrumb from "../common/Breadcrumb";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import Breadcrumb from "../common/Breadcrumb";
|
||||
import { apiTenant } from "./apiTenant";
|
||||
import { useCreateTenant } from "./useTenants";
|
||||
import TenantSubscription from "./TenantSubscription";
|
||||
|
||||
const defaultAvatar = "https://via.placeholder.com/100x100.png?text=Avatar";
|
||||
|
||||
const initialData = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@ -24,64 +24,84 @@ const initialData = {
|
||||
onBoardingDate: "",
|
||||
};
|
||||
|
||||
const RequiredLabel = ({ label, name }) => (
|
||||
<label htmlFor={name} className="form-label small mb-1">
|
||||
{label} <span className="text-danger">*</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
const validateForm = (form, step) => {
|
||||
let errors = {};
|
||||
let fieldsToValidate = [];
|
||||
|
||||
if (step === 1) {
|
||||
fieldsToValidate = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phone",
|
||||
"billingAddress",
|
||||
];
|
||||
} else if (step === 2) {
|
||||
fieldsToValidate = [
|
||||
"organizationName",
|
||||
"organizationSize",
|
||||
"industryId",
|
||||
"reference",
|
||||
"domainName",
|
||||
"onBoardingDate",
|
||||
];
|
||||
}
|
||||
|
||||
fieldsToValidate.forEach((field) => {
|
||||
if (!form?.[field] || String(form?.[field]).trim() === "") {
|
||||
const fieldName = field.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
||||
errors[field] = `${fieldName} is required.`;
|
||||
}
|
||||
});
|
||||
|
||||
if (step === 1 && form.phone && !/^[0-9]{10}$/.test(form.phone)) {
|
||||
errors.phone = "Phone number must be a 10-digit number.";
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const CreateTenant = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const formData = location.state?.formData || null;
|
||||
|
||||
const { createTenant, updateTenant, loading, error, success } = useCreateTenant();
|
||||
const { state } = useLocation();
|
||||
const formData = state?.formData || null;
|
||||
const { createTenant, updateTenant, loading } = useCreateTenant();
|
||||
|
||||
const [form, setForm] = useState(initialData);
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [imagePreview, setImagePreview] = useState(defaultAvatar);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [showImageModal, setShowImageModal] = useState(false);
|
||||
const [showImageSizeModal, setShowImageSizeModal] = useState(false);
|
||||
const [industryOptions, setIndustryOptions] = useState([]);
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// Load form data if it's passed via location state
|
||||
useEffect(() => {
|
||||
if (formData) {
|
||||
const { contactName, contactNumber, logoImage, ...rest } = formData;
|
||||
|
||||
let firstName = "";
|
||||
let lastName = "";
|
||||
if (contactName) {
|
||||
const nameParts = contactName.trim().split(" ");
|
||||
firstName = nameParts.shift() || "";
|
||||
lastName = nameParts.join(" ") || "";
|
||||
}
|
||||
|
||||
setForm({
|
||||
...initialData,
|
||||
...rest,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: contactNumber || "",
|
||||
});
|
||||
|
||||
if (logoImage) {
|
||||
setImagePreview(logoImage);
|
||||
}
|
||||
const [firstName, ...lastNameParts] = (contactName || "").trim().split(" ");
|
||||
const lastName = lastNameParts.join(" ");
|
||||
setForm({ ...initialData, ...rest, firstName, lastName, phone: contactNumber || "" });
|
||||
if (logoImage) setImagePreview(logoImage);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
// Load industry options from the API when the component mounts
|
||||
useEffect(() => {
|
||||
const fetchIndustries = async () => {
|
||||
try {
|
||||
const res = await apiTenant.getIndustries();
|
||||
if (Array.isArray(res.data)) {
|
||||
setIndustryOptions(res.data);
|
||||
const { data } = await apiTenant.getIndustries();
|
||||
if (Array.isArray(data)) {
|
||||
setIndustryOptions(data);
|
||||
if (formData?.industry) {
|
||||
const matchedIndustry = res.data.find(
|
||||
(industry) => industry.name === formData.industry.name
|
||||
);
|
||||
if (matchedIndustry) {
|
||||
setForm((prev) => ({ ...prev, industryId: matchedIndustry.id }));
|
||||
}
|
||||
const matchedIndustry = data.find((i) => i.name === formData.industry.name);
|
||||
if (matchedIndustry) setForm((prev) => ({ ...prev, industryId: matchedIndustry.id }));
|
||||
}
|
||||
} else {
|
||||
console.error("Unexpected response format for industries", res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load industries:", err);
|
||||
@ -92,23 +112,28 @@ const CreateTenant = () => {
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
const sanitizedValue = name === "phone" ? value.replace(/\D/g, "") : value;
|
||||
setForm((prev) => ({ ...prev, [name]: sanitizedValue }));
|
||||
if (formErrors?.[name]) {
|
||||
setFormErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors?.[name];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
if (file.size <= 200 * 1024) {
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
setShowImageSizeModal(true);
|
||||
}
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (file.size > 200 * 1024) {
|
||||
setShowImageSizeModal(true);
|
||||
return;
|
||||
}
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => setImagePreview(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleImageReset = () => {
|
||||
@ -118,355 +143,234 @@ const CreateTenant = () => {
|
||||
|
||||
const handleClearForm = () => {
|
||||
setForm(initialData);
|
||||
setImagePreview(defaultAvatar);
|
||||
setImageFile(null);
|
||||
setFormErrors({});
|
||||
handleImageReset();
|
||||
setStep(1);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const errors = validateForm(form, step);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors);
|
||||
return;
|
||||
}
|
||||
setFormErrors({});
|
||||
setStep((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Determine the image to send to the API
|
||||
// If there's a new file selected (imageFile), use it.
|
||||
// If the image was reset (imagePreview is defaultAvatar), send null.
|
||||
// Otherwise, use the existing logo image (imagePreview).
|
||||
const finalLogoImage = imageFile
|
||||
? imageFile
|
||||
: imagePreview === defaultAvatar
|
||||
? null
|
||||
: imagePreview;
|
||||
|
||||
const errors = validateForm(form, 2);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors);
|
||||
setStep(2);
|
||||
return;
|
||||
}
|
||||
setFormErrors({});
|
||||
const finalLogoImage = imageFile || (imagePreview === defaultAvatar ? null : imagePreview);
|
||||
const submissionData = {
|
||||
...form,
|
||||
logoImage: finalLogoImage,
|
||||
contactNumber: form.phone,
|
||||
contactName: `${form.firstName} ${form.lastName}`.trim(),
|
||||
};
|
||||
|
||||
let result;
|
||||
if (formData?.id) {
|
||||
result = await updateTenant(formData.id, submissionData);
|
||||
|
||||
if (result) {
|
||||
alert("Tenant updated successfully!");
|
||||
navigate("/tenant/profile", { state: { newTenant: result } });
|
||||
} else {
|
||||
alert("Failed to update tenant. Please check the form and try again.");
|
||||
}
|
||||
if (result) navigate("/tenant/profile", { state: { newTenant: result } });
|
||||
} else {
|
||||
result = await createTenant(submissionData);
|
||||
|
||||
if (result) {
|
||||
alert("Tenant created successfully!");
|
||||
navigate("/tenant/profile/subscription", { state: { formData: result } });
|
||||
} else {
|
||||
alert("Failed to create tenant. Please check the form and try again.");
|
||||
}
|
||||
if (result) navigate("/tenant/profile/viewtenant", { state: { formData: result } });
|
||||
}
|
||||
},
|
||||
[form, imagePreview, imageFile, formData, navigate, createTenant, updateTenant]
|
||||
);
|
||||
|
||||
const RequiredLabel = ({ label }) => (
|
||||
<label className="form-label small mb-1">
|
||||
{label} <span className="text-danger">*</span>
|
||||
</label>
|
||||
);
|
||||
const renderFormStep = () => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
<>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="First Name" name="firstName" />
|
||||
<input id="firstName" type="text" name="firstName" className="form-control form-control-sm" value={form.firstName} onChange={handleChange} required />
|
||||
{formErrors?.firstName && (<p className="text-danger small mt-1">{formErrors.firstName}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Last Name" name="lastName" />
|
||||
<input id="lastName" type="text" name="lastName" className="form-control form-control-sm" value={form.lastName} onChange={handleChange} required />
|
||||
{formErrors?.lastName && (<p className="text-danger small mt-1">{formErrors.lastName}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Email" name="email" />
|
||||
<input id="email" type="email" name="email" className="form-control form-control-sm" value={form.email} onChange={handleChange} required />
|
||||
{formErrors?.email && (<p className="text-danger small mt-1">{formErrors.email}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Phone" name="phone" />
|
||||
<input id="phone" type="text" name="phone" className="form-control form-control-sm" value={form.phone} onChange={handleChange} required maxLength="10" />
|
||||
{formErrors?.phone && (<p className="text-danger small mt-1">{formErrors.phone}</p>)}
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<RequiredLabel label="Billing Address" name="billingAddress" />
|
||||
<textarea id="billingAddress" name="billingAddress" className="form-control form-control-sm" rows="2" value={form.billingAddress} onChange={handleChange} required></textarea>
|
||||
{formErrors?.billingAddress && (<p className="text-danger small mt-1">{formErrors.billingAddress}</p>)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Onboarding Date" name="onBoardingDate" />
|
||||
<input id="onBoardingDate" type="date" name="onBoardingDate" className="form-control form-control-sm" value={form.onBoardingDate} onChange={handleChange} required />
|
||||
{formErrors?.onBoardingDate && (<p className="text-danger small mt-1">{formErrors.onBoardingDate}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Organization Name" name="organizationName" />
|
||||
<input id="organizationName" type="text" name="organizationName" className="form-control form-control-sm" value={form.organizationName} onChange={handleChange} required />
|
||||
{formErrors?.organizationName && (<p className="text-danger small mt-1">{form.organizationName}</p>)}
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Organization Size" name="organizationSize" />
|
||||
<select id="organizationSize" name="organizationSize" className="form-select form-select-sm" value={form.organizationSize} onChange={handleChange} required>
|
||||
<option value="">Select</option>
|
||||
<option>1-50</option>
|
||||
<option>51-100</option>
|
||||
<option>101-500</option>
|
||||
<option>500+</option>
|
||||
</select>
|
||||
{formErrors?.organizationSize && (<p className="text-danger small mt-1">{formErrors.organizationSize}</p>)}
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Industry" name="industryId" />
|
||||
<select id="industryId" name="industryId" className="form-select form-select-sm" value={form.industryId} onChange={handleChange} required>
|
||||
<option value="">Select</option>
|
||||
{industryOptions.map((industry) => (<option key={industry.id} value={industry.id}>{industry.name}</option>))}
|
||||
</select>
|
||||
{formErrors?.industryId && (<p className="text-danger small mt-1">{formErrors.industryId}</p>)}
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Reference" name="reference" />
|
||||
<select id="reference" name="reference" className="form-select form-select-sm" value={form.reference} onChange={handleChange} required>
|
||||
<option value="">Select</option>
|
||||
<option>Google</option>
|
||||
<option>Friend</option>
|
||||
<option>Advertisement</option>
|
||||
<option>Root Tenant</option>
|
||||
</select>
|
||||
{formErrors?.reference && (<p className="text-danger small mt-1">{formErrors.reference}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="taxId" className="form-label small mb-1">Tax ID</label>
|
||||
<input id="taxId" type="text" name="taxId" className="form-control form-control-sm" value={form.taxId} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Domain Name" name="domainName" />
|
||||
<input id="domainName" type="text" name="domainName" className="form-control form-control-sm" value={form.domainName} onChange={handleChange} required />
|
||||
{formErrors?.domainName && (<p className="text-danger small mt-1">{formErrors.domainName}</p>)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="mobile" className="form-label small mb-1">Landline Number</label>
|
||||
<input id="mobile" type="text" name="mobile" className="form-control form-control-sm" value={form.mobile} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label htmlFor="description" className="form-label small mb-1">Description</label>
|
||||
<textarea id="description" name="description" className="form-control form-control-sm" rows="2" value={form.description} onChange={handleChange}></textarea>
|
||||
</div>
|
||||
<div className="mb-0 text-start d-flex align-items-start gap-3 position-relative">
|
||||
<div style={{ position: "relative", width: "100px", height: "100px" }}>
|
||||
<img src={imagePreview} alt="Profile Preview" onClick={() => setShowImageModal(true)} style={{ width: "100px", height: "100px", objectFit: "cover", borderRadius: "8px", border: "1px solid #ccc", cursor: "pointer", }} />
|
||||
<button type="button" className="btn btn-sm btn-light position-absolute" onClick={handleImageReset} style={{ top: "-10px", right: "-10px", padding: "0.25rem 0.5rem", borderRadius: "50%", boxShadow: "0 0 3px rgba(0,0,0,0.3)", }} title="Delete Photo"><i className="bx bx-trash text-danger"></i></button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<input type="file" accept="image/png, image/jpeg, image/gif" onChange={handleImageChange} style={{ display: "none" }} id="upload-photo" />
|
||||
<label htmlFor="upload-photo" className="btn btn-sm btn-primary me-2">Upload Logo</label>
|
||||
</div>
|
||||
<small className="text-muted">Allowed JPG, GIF or PNG. Max size of 200KB</small>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<div className="col-12">
|
||||
<TenantSubscription />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ label: "Contact Info", subtitle: "Provide contact details", icon: "bx-user" },
|
||||
{ label: "Organization Details", subtitle: "Enter organization info", icon: "bx-buildings" },
|
||||
{ label: "Subscription", subtitle: "Select a plan", icon: "bx-credit-card" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container py-3">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/" },
|
||||
{ label: "Tenant", link: "/tenant/profile" },
|
||||
{ label: formData?.id ? "Update Tenant" : "Create Tenant" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Tenant", link: "/tenant/profile" }, { label: formData?.id ? "Update Tenant" : "Create Tenant" }]} />
|
||||
<div className="card rounded-3 shadow-sm mt-3">
|
||||
<div className="card-body p-3 mx-6">
|
||||
<h5 className="text-start mb-3">
|
||||
{formData?.id ? "Update Tenant" : "Create Tenant"}
|
||||
</h5>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row g-4 text-start">
|
||||
{/* Form fields */}
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="First Name" />
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.firstName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Last Name" />
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.lastName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Email" />
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="form-control form-control-sm"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<RequiredLabel label="Phone" />
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
className="form-control form-control-sm"
|
||||
value={form.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small mb-1">Landline Number</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mobile"
|
||||
className="form-control form-control-sm"
|
||||
value={form.mobile}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Onboarding Date" />
|
||||
<input
|
||||
type="date"
|
||||
name="onBoardingDate"
|
||||
className="form-control form-control-sm"
|
||||
value={form.onBoardingDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Organization Name" />
|
||||
<input
|
||||
type="text"
|
||||
name="organizationName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.organizationName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Organization Size" />
|
||||
<select
|
||||
name="organizationSize"
|
||||
className="form-select form-select-sm"
|
||||
value={form.organizationSize}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option>1-50</option>
|
||||
<option>51-100</option>
|
||||
<option>101-500</option>
|
||||
<option>500+</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Industry" />
|
||||
<select
|
||||
name="industryId"
|
||||
className="form-select form-select-sm"
|
||||
value={form.industryId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select</option>
|
||||
{industryOptions.map((industry) => (
|
||||
<option key={industry.id} value={industry.id}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Reference" />
|
||||
<select
|
||||
name="reference"
|
||||
className="form-select form-select-sm"
|
||||
value={form.reference}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option>Google</option>
|
||||
<option>Friend</option>
|
||||
<option>Advertisement</option>
|
||||
<option>Root Tenant</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label small mb-1">Tax ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="taxId"
|
||||
className="form-control form-control-sm"
|
||||
value={form.taxId}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Domain Name" />
|
||||
<input
|
||||
type="text"
|
||||
name="domainName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.domainName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<RequiredLabel label="Billing Address" />
|
||||
<textarea
|
||||
name="billingAddress"
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
value={form.billingAddress}
|
||||
onChange={handleChange}
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label small mb-1">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
value={form.description}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mb-0 text-start d-flex align-items-start gap-3 position-relative">
|
||||
<div style={{ position: "relative", width: "100px", height: "100px" }}>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile Preview"
|
||||
onClick={() => setShowImageModal(true)}
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 className="text-start mb-0">{formData?.id ? "Update Tenant" : "Create Tenant"}</h5>
|
||||
{step !== 3 && (<button type="button" className="btn btn-sm btn-warning" onClick={handleClearForm}>Clear</button>)}
|
||||
</div>
|
||||
<div className="d-flex flex-column flex-md-row align-items-start">
|
||||
{/* Steps Container with vertical divider */}
|
||||
<div className="d-flex flex-column align-items-start me-md-4 mb-3 mb-md-0 pe-md-4 border-end">
|
||||
{steps.map((s, index) => (
|
||||
<div key={index} className="d-flex align-items-center position-relative py-2 py-md-3">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-light position-absolute"
|
||||
onClick={handleImageReset}
|
||||
style={{
|
||||
top: "-10px",
|
||||
right: "-10px",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 0 3px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
title="Delete Photo"
|
||||
className={`btn btn-link p-0 text-start ${step >= index + 1 ? 'text-primary' : 'text-dark'}`}
|
||||
style={{ cursor: step >= index + 1 ? 'pointer' : 'default', textDecoration: 'none' }}
|
||||
onClick={() => setStep(index + 1)}
|
||||
disabled={step < index + 1}
|
||||
>
|
||||
<i className="bx bx-trash text-danger"></i>
|
||||
<div className="d-flex align-items-center">
|
||||
<div
|
||||
className={`d-flex align-items-center justify-content-center me-3 ${step === index + 1 ? 'bg-primary text-white' : 'bg-light text-secondary'} ${step > index + 1 ? 'bg-white text-primary border border-primary' : ''}`}
|
||||
style={{ width: '40px', height: '40px', borderRadius: '8px' }}
|
||||
>
|
||||
<i className={`bx ${s.icon}`}></i>
|
||||
</div>
|
||||
<div className="d-flex flex-column text-start">
|
||||
<span className={`small ${step === index + 1 ? 'fw-semibold' : 'text-muted'}`}>{s.label}</span>
|
||||
<span className="text-muted small">{s.subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
onChange={handleImageChange}
|
||||
style={{ display: "none" }}
|
||||
id="upload-photo"
|
||||
/>
|
||||
<label htmlFor="upload-photo" className="btn btn-sm btn-primary me-2">
|
||||
Upload New Photo
|
||||
</label>
|
||||
</div>
|
||||
<small className="text-muted">Allowed JPG, GIF or PNG. Max size of 200KB</small>
|
||||
))}
|
||||
</div>
|
||||
{/* Form Content */}
|
||||
<div className="tab-content w-100 p-md-3 pt-md-0">
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className="row g-4 text-start">{renderFormStep()}</div>
|
||||
<div className="mt-4 d-flex justify-content-end">
|
||||
{step > 1 && (<button type="button" className="btn btn-sm btn-secondary me-2 px-4" onClick={() => setStep((prev) => prev - 1)}>Previous</button>)}
|
||||
{step < 3 && (<button type="button" className="btn btn-sm btn-primary px-4" onClick={handleNext} disabled={loading}>Next</button>)}
|
||||
{step === 3 && (<button type="submit" className="btn btn-sm btn-primary px-4" disabled={loading}>{loading ? "Saving..." : "Save & Continue"}</button>)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary px-4"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : formData?.id ? "Update" : "Save & Continue"}
|
||||
</button>
|
||||
|
||||
{!formData?.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-warning ms-2 px-4"
|
||||
onClick={handleClearForm}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
|
||||
{formData?.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary ms-2 px-4"
|
||||
onClick={() => navigate("/tenant/profile")}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal show={showImageModal} onHide={() => setShowImageModal(false)} centered size="lg">
|
||||
<Modal.Body className="text-center">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
style={{ width: "100%", height: "auto", borderRadius: "8px" }}
|
||||
/>
|
||||
<img src={imagePreview} alt="Preview" style={{ width: "100%", height: "auto", borderRadius: "8px" }} />
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showImageSizeModal} onHide={() => setShowImageSizeModal(false)} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Image Size Warning</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
The selected image file must be less than 200KB. Please choose a smaller file.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn btn-primary" onClick={() => setShowImageSizeModal(false)}>
|
||||
Close
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
<Modal.Header closeButton><Modal.Title>Image Size Warning</Modal.Title></Modal.Header>
|
||||
<Modal.Body>The selected image file must be less than 200KB. Please choose a smaller file.</Modal.Body>
|
||||
<Modal.Footer><button className="btn btn-primary" onClick={() => setShowImageSizeModal(false)}>Close</button></Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -11,25 +11,20 @@ const Tenant = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// This useEffect syncs the fetched tenants with the local state.
|
||||
useEffect(() => {
|
||||
if (tenants) {
|
||||
setLocalTenants(tenants);
|
||||
}
|
||||
}, [tenants]);
|
||||
|
||||
// This useEffect handles adding or updating a tenant from the location state.
|
||||
useEffect(() => {
|
||||
const newTenant = location.state?.newTenant;
|
||||
if (newTenant) {
|
||||
setLocalTenants((prev) => {
|
||||
// Find if the tenant already exists by ID
|
||||
const tenantIndex = prev.findIndex((t) => t.id === newTenant.id);
|
||||
if (tenantIndex > -1) {
|
||||
// If exists, update the tenant
|
||||
return prev.map((t) => (t.id === newTenant.id ? newTenant : t));
|
||||
} else {
|
||||
// If not, add a new tenant with a generated ID
|
||||
return [...prev, { ...newTenant, id: newTenant.id || Date.now() }];
|
||||
}
|
||||
});
|
||||
@ -48,17 +43,14 @@ const Tenant = () => {
|
||||
navigate("/tenant/profile/viewtenant", { state: { formData: tenant } });
|
||||
};
|
||||
|
||||
// This function handles tenant deletion from the local state.
|
||||
const handleDelete = (id) => {
|
||||
if (window.confirm("Are you sure you want to delete this tenant?")) {
|
||||
setLocalTenants((prev) => prev.filter((t) => t.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function for case-insensitive and whitespace-insensitive search
|
||||
const normalize = (str) => (str?.toLowerCase().replace(/\s+/g, "") || "");
|
||||
|
||||
// Filters the tenants based on the search term
|
||||
const filteredTenants = localTenants.filter((tenant) => {
|
||||
const term = normalize(searchTerm);
|
||||
return (
|
||||
@ -67,7 +59,7 @@ const Tenant = () => {
|
||||
normalize(tenant.contactNumber).includes(term) ||
|
||||
normalize(tenant.domainName).includes(term) ||
|
||||
normalize(tenant.name).includes(term) ||
|
||||
normalize(tenant.oragnizationSize).includes(term) ||
|
||||
normalize(tenant.organizationSize).includes(term) ||
|
||||
normalize(tenant.industry?.name).includes(term)
|
||||
);
|
||||
});
|
||||
@ -98,18 +90,21 @@ const Tenant = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive p-3 pt-3">
|
||||
<table className="table text-start align-middle">
|
||||
<div
|
||||
className="table-responsive p-3 pt-3"
|
||||
style={{ overflowX: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<table className="table text-start align-middle mb-0" style={{ minWidth: "1000px" }}>
|
||||
<thead>
|
||||
<tr className="fs-6">
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Domain</th>
|
||||
<th>Organization</th>
|
||||
<th>Size</th>
|
||||
<th>Industry</th>
|
||||
<th>Actions</th>
|
||||
<th className="px-4">Organization</th>
|
||||
<th className="px-4">Name</th>
|
||||
{/* <th className="px-4">Email</th> */}
|
||||
<th className="px-4">Phone</th>
|
||||
{/* <th className="px-4">Domain</th> */}
|
||||
<th className="px-4">Size</th>
|
||||
<th className="px-4">Industry</th>
|
||||
<th className="px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0">
|
||||
@ -120,7 +115,8 @@ const Tenant = () => {
|
||||
|
||||
return (
|
||||
<tr key={tenant.id} style={{ height: "50px" }} className="align-middle">
|
||||
<td>
|
||||
<td className="px-4"><i className="bx bx-building text-secondary me-2"></i>{tenant.name}</td>
|
||||
<td className="px-4">
|
||||
<div className={`d-flex align-items-center ${tenant.logoImage ? "gap-2" : ""}`}>
|
||||
{tenant.logoImage ? (
|
||||
<div
|
||||
@ -152,39 +148,37 @@ const Tenant = () => {
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td><i className="bx bx-envelope text-primary me-2"></i>{tenant.email}</td>
|
||||
<td><i className="bx bx-phone text-success me-2"></i>{tenant.contactNumber}</td>
|
||||
<td><i className="bx bx-globe text-info me-2"></i>{tenant.domainName}</td>
|
||||
<td><i className="bx bx-building text-secondary me-2"></i>{tenant.name}</td>
|
||||
<td><i className="bx bx-group text-warning me-2"></i>{tenant.oragnizationSize}</td>
|
||||
<td><i className="bx bx-briefcase-alt text-dark me-2"></i>{tenant.industry?.name}</td>
|
||||
<td>
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-sm text-secondary"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="bx bx-dots-horizontal-rounded fs-4"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleView(tenant)}>
|
||||
<i className="bx bx-show me-2 text-secondary"></i> View
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleEdit(tenant)}>
|
||||
<i className="bx bx-edit-alt me-2 text-primary"></i> Edit
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleDelete(tenant.id)}>
|
||||
<i className="bx bx-trash me-2 text-danger"></i> Delete
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/* <td className="px-4"><i className="bx bx-envelope text-primary me-2"></i>{tenant.email}</td> */}
|
||||
<td className="px-4"><i className="bx bx-phone text-success me-2"></i>{tenant.contactNumber}</td>
|
||||
{/* <td className="px-4"><i className="bx bx-globe text-info me-2"></i>{tenant.domainName}</td> */}
|
||||
{/* <td className="px-4">
|
||||
{tenant.domainName ? (
|
||||
<>
|
||||
<i className="bx bx-globe text-info me-2"></i>
|
||||
{tenant.domainName}
|
||||
</>
|
||||
) : (
|
||||
<span className="d-block text-center w-50">--</span>
|
||||
)}
|
||||
</td> */}
|
||||
|
||||
|
||||
<td className="px-4"><i className="bx bx-group text-warning me-2"></i>{tenant.organizationSize}</td>
|
||||
<td className="px-4"><i className="bx bx-briefcase-alt text-dark me-2"></i>{tenant.industry?.name}</td>
|
||||
<td className="px-4">
|
||||
<div className="d-flex gap-2">
|
||||
<i
|
||||
className="bx bx-show text-secondary fs-5 cursor-pointer"
|
||||
onClick={() => handleView(tenant)}
|
||||
></i>
|
||||
<i
|
||||
className="bx bx-edit-alt text-primary fs-5 cursor-pointer"
|
||||
onClick={() => handleEdit(tenant)}
|
||||
></i>
|
||||
<i
|
||||
className="bx bx-trash text-danger fs-5 cursor-pointer"
|
||||
onClick={() => handleDelete(tenant.id)}
|
||||
></i>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
@ -1,84 +1,310 @@
|
||||
import React from "react";
|
||||
import Breadcrumb from "../common/Breadcrumb"; // ✅ Adjust path if needed
|
||||
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 = () => (
|
||||
<nav aria-label="breadcrumb">
|
||||
{/* <ol className="breadcrumb">
|
||||
<li className="breadcrumb-item"><a href="#">Home</a></li>
|
||||
<li className="breadcrumb-item"><a href="#">Subscriptions</a></li>
|
||||
<li className="breadcrumb-item active" aria-current="page">Pricing</li>
|
||||
</ol> */}
|
||||
</nav>
|
||||
);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
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: "$40",
|
||||
per: "/month",
|
||||
description: "For small to medium businesses",
|
||||
subText: "USD 480/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: "$80",
|
||||
per: "/month",
|
||||
description: "Solution for big organizations",
|
||||
subText: "USD 960/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 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 selectedBillingKey = billingOptions[selectedBillingIndex];
|
||||
const plansForBilling = pricingData[selectedBillingKey];
|
||||
|
||||
const currentPlan = plansForBilling ? plansForBilling[selectedPlanIndex] : null;
|
||||
|
||||
return (
|
||||
<div className="container py-1">
|
||||
{/* ✅ Breadcrumb */}
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/" },
|
||||
{ label: "Tenant", link: "/tenant/profile" },
|
||||
{ label: "Subscription" },
|
||||
]}
|
||||
/>
|
||||
<Breadcrumb />
|
||||
|
||||
<div className="my-4">
|
||||
<p className="mb-2">Choose your billing cycle:</p>
|
||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-sm">
|
||||
{billingOptions.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`btn px-4 py-2 rounded-0 ${
|
||||
selectedBillingIndex === index ? "btn-primary text-white" : "btn-light"
|
||||
}`}
|
||||
style={{
|
||||
borderRight:
|
||||
index !== billingOptions.length - 1 ? "1px solid #dee2e6" : "none",
|
||||
fontWeight: selectedBillingIndex === index ? "600" : "normal",
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedBillingIndex(index);
|
||||
setSelectedPlanIndex(null);
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plansForBilling && (
|
||||
<div className="my-4">
|
||||
<p className="mb-2">Select a plan:</p>
|
||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-sm">
|
||||
{plansForBilling.map((plan, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`btn px-4 py-2 rounded-0 ${
|
||||
selectedPlanIndex === index ? "btn-primary text-white" : "btn-light"
|
||||
}`}
|
||||
style={{
|
||||
borderRight:
|
||||
index !== plansForBilling.length - 1 ? "1px solid #dee2e6" : "none",
|
||||
fontWeight: selectedPlanIndex === index ? "600" : "normal",
|
||||
}}
|
||||
onClick={() => setSelectedPlanIndex(index)}
|
||||
>
|
||||
{plan.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row justify-content-center mt-3">
|
||||
{plans.map((plan, idx) => (
|
||||
<div key={idx} className="col-md-4 mb-4">
|
||||
<div className="row justify-content-start mt-3">
|
||||
{currentPlan && (
|
||||
<div className="col-md-6 mb-4">
|
||||
<div
|
||||
className={`card h-100 shadow-sm ${
|
||||
plan.highlight ? "border-primary" : ""
|
||||
currentPlan.highlight ? "border-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="card-body text-center position-relative">
|
||||
{plan.badge && (
|
||||
<div className="card-body position-relative">
|
||||
{currentPlan.badge && (
|
||||
<span className="badge bg-primary position-absolute top-0 end-0 m-2">
|
||||
{plan.badge}
|
||||
{currentPlan.badge}
|
||||
</span>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
@ -87,28 +313,47 @@ const TenantSubscription = () => {
|
||||
style={{ fontSize: "48px", color: "#6366f1" }}
|
||||
></i>
|
||||
</div>
|
||||
<h5 className="card-title">{plan.name}</h5>
|
||||
<p className="text-muted">{plan.description}</p>
|
||||
<h5 className="card-title">{currentPlan.name}</h5>
|
||||
<p className="text-muted">{currentPlan.description}</p>
|
||||
<h2 className="fw-bold">
|
||||
{plan.price} <small className="fs-6">{plan.per}</small>
|
||||
{currentPlan.price}{" "}
|
||||
<small className="fs-6">{currentPlan.per}</small>
|
||||
</h2>
|
||||
{plan.subText && <p className="text-muted">{plan.subText}</p>}
|
||||
<ul className="list-unstyled text-start mt-4 mb-4">
|
||||
{plan.features.map((feat, i) => (
|
||||
{currentPlan.subText && <p className="text-muted">{currentPlan.subText}</p>}
|
||||
<ul className="list-unstyled mt-4 mb-4">
|
||||
{currentPlan.features.map((feat, i) => (
|
||||
<li key={i} className="mb-2">
|
||||
<i className="bx bx-check text-success me-2"></i>
|
||||
{feat}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={plan.buttonClass}>{plan.button}</button>
|
||||
<button className={currentPlan.buttonClass}>
|
||||
{currentPlan.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Moved the Organization Size dropdown to appear after the details section */}
|
||||
<div className="my-4">
|
||||
<label htmlFor="organizationSize" className="form-label mb-2">Organization Size</label>
|
||||
<select
|
||||
id="organizationSize"
|
||||
className="form-select w-auto shadow-sm"
|
||||
value={selectedOrgSize}
|
||||
onChange={(e) => setSelectedOrgSize(e.target.value)}
|
||||
>
|
||||
<option value="" disabled>Select a size</option>
|
||||
{organizationSizes.map((size, index) => (
|
||||
<option key={index} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TenantSubscription;
|
||||
export default TenantSubscription;
|
||||
@ -46,7 +46,7 @@ const ViewTenant = () => {
|
||||
aria-controls="profile-details"
|
||||
aria-selected={activeTab === "profile"}
|
||||
>
|
||||
<i className="bx bx-user me-1"></i>Profile Details
|
||||
<i className="bx bx-user me-1"></i>Tenant Profile
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item" role="presentation">
|
||||
@ -79,7 +79,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-user bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-user bx-xs me-2 mt-0"></i>
|
||||
<strong>Contact Name</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "28px" }}>:</span>
|
||||
@ -90,7 +90,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-envelope bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-envelope bx-xs me-2 mt-0"></i>
|
||||
<strong>Email</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "84px" }}>:</span>
|
||||
@ -101,7 +101,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-phone bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-phone bx-xs me-2 mt-0"></i>
|
||||
<strong>Phone</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "80px" }}>:</span>
|
||||
@ -112,7 +112,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-globe bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-globe bx-xs me-2 mt-0"></i>
|
||||
<strong>Domain</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "72px" }}>:</span>
|
||||
@ -123,7 +123,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-building bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-building bx-xs me-2 mt-0"></i>
|
||||
<strong>Organization</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "38px" }}>:</span>
|
||||
@ -134,18 +134,18 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-group bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-group bx-xs me-2 mt-0"></i>
|
||||
<strong>Size</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "92px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.oragnizationSize || "N/A"}</span>
|
||||
<span>{tenant.organizationSize || "N/A"}</span>
|
||||
</div>
|
||||
{/* Industry */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-briefcase bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-briefcase bx-xs me-2 mt-0"></i>
|
||||
<strong>Industry</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "65px" }}>:</span>
|
||||
@ -156,7 +156,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-0"></i>
|
||||
<strong>Status</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "77px" }}>:</span>
|
||||
@ -182,7 +182,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-credit-card-alt bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-credit-card-alt bx-xs me-2 mt-0"></i>
|
||||
<strong>Plan</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "72px" }}>:</span>
|
||||
@ -193,7 +193,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-calendar bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-calendar bx-xs me-2 mt-0"></i>
|
||||
<strong>Start Date</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "35px" }}>:</span>
|
||||
@ -204,7 +204,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-calendar-x bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-calendar-x bx-xs me-2 mt-0"></i>
|
||||
<strong>End Date</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "44px" }}>:</span>
|
||||
@ -215,7 +215,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-0"></i>
|
||||
<strong>Status</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "59px" }}>:</span>
|
||||
@ -226,7 +226,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-chair bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-chair bx-xs me-2 mt-0"></i>
|
||||
<strong>Seats</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "65px" }}>:</span>
|
||||
@ -237,7 +237,7 @@ const ViewTenant = () => {
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-money bx-xs me-2 mt-1"></i>
|
||||
<i className="bx bx-money bx-xs me-2 mt-0"></i>
|
||||
<strong>Billing</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "61px" }}>:</span>
|
||||
|
||||
@ -234,58 +234,7 @@ const EmployeeList = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{EmpForManageRole && (
|
||||
<GlobalModel
|
||||
isOpen={EmpForManageRole}
|
||||
closeModal={() => setEmpForManageRole(null)}
|
||||
>
|
||||
<ManageEmp
|
||||
employeeId={EmpForManageRole}
|
||||
onClosed={() => setEmpForManageRole(null)}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<GlobalModel
|
||||
isOpen={showModal}
|
||||
size="lg"
|
||||
closeModal={() => setShowModal(false)}
|
||||
>
|
||||
<ManageEmployee
|
||||
employeeId={selectedEmployeeId}
|
||||
onClosed={() => setShowModal(false)}
|
||||
IsAllEmployee={showAllEmployees}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{IsDeleteModalOpen && (
|
||||
<div
|
||||
className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: IsDeleteModalOpen ? "block" : "none",
|
||||
backgroundColor: IsDeleteModalOpen
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: "transparent",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Suspend Employee"}
|
||||
message={"Are you sure you want delete?"}
|
||||
onSubmit={suspendEmployee}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={employeeLodaing}
|
||||
paramData={selectedEmpFordelete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container-fluid">
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
@ -306,44 +255,10 @@ const EmployeeList = () => {
|
||||
{/* Switches: All Employees + Inactive */}
|
||||
<div className="d-flex flex-wrap align-items-center gap-3">
|
||||
{/* All Employees Switch */}
|
||||
{ViewAllEmployee && (
|
||||
<div className="form-check form-switch text-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="allEmployeesCheckbox"
|
||||
checked={showAllEmployees}
|
||||
onChange={handleAllEmployeesToggle}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label ms-0"
|
||||
htmlFor="allEmployeesCheckbox"
|
||||
>
|
||||
All Employees
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Show Inactive Employees Switch */}
|
||||
{showAllEmployees && (
|
||||
<div className="form-check form-switch text-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
checked={showInactive}
|
||||
onChange={(e)=> setShowInactive(e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label ms-0"
|
||||
htmlFor="inactiveEmployeesCheckbox"
|
||||
>
|
||||
Show Inactive Employees
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right side: Search + Export + Add Employee */}
|
||||
@ -363,69 +278,9 @@ const EmployeeList = () => {
|
||||
</div>
|
||||
|
||||
{/* Export Dropdown */}
|
||||
<div className="dropdown">
|
||||
<button
|
||||
aria-label="Click me"
|
||||
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="bx bx-export me-2 bx-sm"></i>Export
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={() => handleExport("print")}
|
||||
>
|
||||
<i className="bx bx-printer me-1"></i> Print
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={() => handleExport("csv")}
|
||||
>
|
||||
<i className="bx bx-file me-1"></i> CSV
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={() => handleExport("excel")}
|
||||
>
|
||||
<i className="bx bxs-file-export me-1"></i> Excel
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={() => handleExport("pdf")}
|
||||
>
|
||||
<i className="bx bxs-file-pdf me-1"></i> PDF
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Add Employee Button */}
|
||||
{Manage_Employee && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
onClick={() => handleEmployeeModel(null)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
<span className="d-none d-md-inline-block">
|
||||
Add New Employee
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user