marco.pms.web/src/components/Tenant/CreateTenant.jsx

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;