406 lines
18 KiB
JavaScript
406 lines
18 KiB
JavaScript
import React, { useEffect, useState, useCallback } from "react";
|
|
import { useNavigate, useLocation } from "react-router-dom";
|
|
import { Modal } from "react-bootstrap";
|
|
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 = {
|
|
firstName: "",
|
|
lastName: "",
|
|
email: "",
|
|
phone: "",
|
|
mobile: "",
|
|
domainName: "",
|
|
organizationName: "",
|
|
description: "",
|
|
organizationSize: "",
|
|
industryId: "",
|
|
reference: "",
|
|
taxId: "",
|
|
billingAddress: "",
|
|
onBoardingDate: "",
|
|
status: "",
|
|
};
|
|
|
|
// RequiredLabel component remains the same for mandatory fields
|
|
const RequiredLabel = ({ label, name }) => (
|
|
<label htmlFor={name} className="form-label small mb-1">
|
|
{label} <span className="text-danger">*</span>
|
|
</label>
|
|
);
|
|
|
|
// Regular label for non-mandatory fields
|
|
const RegularLabel = ({ label, name }) => (
|
|
<label htmlFor={name} className="form-label small mb-1">
|
|
{label}
|
|
</label>
|
|
);
|
|
|
|
const validateForm = (form, step) => {
|
|
let errors = {};
|
|
let fieldsToValidate = [];
|
|
|
|
// Updated list of mandatory fields for each step
|
|
if (step === 1) {
|
|
fieldsToValidate = [
|
|
"firstName",
|
|
"lastName",
|
|
"email",
|
|
"phone",
|
|
"billingAddress",
|
|
];
|
|
} else if (step === 2) {
|
|
fieldsToValidate = [
|
|
"organizationName",
|
|
"onBoardingDate",
|
|
"organizationSize",
|
|
"industryId",
|
|
"reference",
|
|
"status",
|
|
];
|
|
}
|
|
|
|
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 { state } = useLocation();
|
|
const formData = state?.formData || null;
|
|
// const { createTenant, updateTenant, loading } = useCreateTenant();
|
|
const { mutate: createTenant, isPending } = useCreateTenant();
|
|
const queryClient = useQueryClient();
|
|
|
|
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);
|
|
|
|
useEffect(() => {
|
|
if (formData) {
|
|
const { contactName, contactNumber, logoImage, ...rest } = formData;
|
|
const [firstName, ...lastNameParts] = (contactName || "").trim().split(" ");
|
|
const lastName = lastNameParts.join(" ");
|
|
setForm({ ...initialData, ...rest, firstName, lastName, phone: contactNumber || "" });
|
|
if (logoImage) setImagePreview(logoImage);
|
|
}
|
|
}, [formData]);
|
|
|
|
useEffect(() => {
|
|
const fetchIndustries = async () => {
|
|
try {
|
|
const { data } = await apiTenant.getIndustries();
|
|
if (Array.isArray(data)) {
|
|
setIndustryOptions(data);
|
|
if (formData?.industry) {
|
|
const matchedIndustry = data.find((i) => i.name === formData.industry.name);
|
|
if (matchedIndustry) setForm((prev) => ({ ...prev, industryId: matchedIndustry.id }));
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load industries:", err);
|
|
}
|
|
};
|
|
fetchIndustries();
|
|
}, [formData]);
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value } = e.target;
|
|
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) 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 = () => {
|
|
setImageFile(null);
|
|
setImagePreview(defaultAvatar);
|
|
};
|
|
|
|
const handleClearForm = () => {
|
|
setForm(initialData);
|
|
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();
|
|
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 tenantPayload = {
|
|
...form,
|
|
logoImage: finalLogoImage,
|
|
contactNumber: form.phone,
|
|
contactName: `${form.firstName} ${form.lastName}`.trim(),
|
|
};
|
|
let result;
|
|
if (formData?.id) {
|
|
// 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 } });
|
|
createTenant(tenantPayload);
|
|
}
|
|
},
|
|
[form, imagePreview, imageFile, formData, navigate, createTenant]
|
|
);
|
|
|
|
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="Contact Number" 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">{formErrors.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">
|
|
<RequiredLabel label="Status" name="status" />
|
|
<select id="status" name="status" className="form-select form-select-sm" value={form.status} onChange={handleChange} required>
|
|
<option value="">Select</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Inactive">Inactive</option>
|
|
</select>
|
|
{formErrors?.status && (<p className="text-danger small mt-1">{formErrors.status}</p>)}
|
|
</div>
|
|
<div className="col-md-6">
|
|
<RegularLabel label="Tax ID" name="taxId" />
|
|
<input id="taxId" type="text" name="taxId" className="form-control form-control-sm" value={form.taxId} onChange={handleChange} />
|
|
{formErrors?.taxId && (<p className="text-danger small mt-1">{formErrors.taxId}</p>)}
|
|
</div>
|
|
<div className="col-md-6">
|
|
<RegularLabel label="Domain Name" name="domainName" />
|
|
<input id="domainName" type="text" name="domainName" className="form-control form-control-sm" value={form.domainName} onChange={handleChange} />
|
|
{formErrors?.domainName && (<p className="text-danger small mt-1">{formErrors.domainName}</p>)}
|
|
</div>
|
|
<div className="col-md-6">
|
|
<RegularLabel label="Landline Number" name="mobile" />
|
|
<input id="mobile" type="text" name="mobile" className="form-control form-control-sm" value={form.mobile} onChange={handleChange} />
|
|
{formErrors?.mobile && (<p className="text-danger small mt-1">{formErrors.mobile}</p>)}
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<RegularLabel label="Description" name="description" />
|
|
<textarea id="description" name="description" className="form-control form-control-sm" rows="2" value={form.description} onChange={handleChange}></textarea>
|
|
{formErrors?.description && (<p className="text-danger small mt-1">{formErrors.description}</p>)}
|
|
</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" }]} />
|
|
<div className="card rounded-3 shadow-sm mt-3">
|
|
<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-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}
|
|
>
|
|
<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>
|
|
{/* 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={isPending}>Next</button>)}
|
|
{step === 3 && (<button type="submit" className="btn btn-sm btn-primary px-4" disabled={isPending}>{isPending ? "Saving..." : "Save & Continue"}</button>)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</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" }} />
|
|
</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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CreateTenant; |