Merge branch 'Refactor_Directory' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Sep_1W_V2

This commit is contained in:
Kartik Sharma 2025-09-16 10:40:27 +05:30
commit 42086d7f3a
116 changed files with 3179 additions and 3544 deletions

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

View File

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

View File

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

View File

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 411 KiB

After

Width:  |  Height:  |  Size: 411 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 787 KiB

After

Width:  |  Height:  |  Size: 787 KiB

View File

Before

Width:  |  Height:  |  Size: 786 KiB

After

Width:  |  Height:  |  Size: 786 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 401 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg idth="64" height="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M0 142.1L0 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-240c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32l0 240c0 17.7 14.3 32 32 32s32-14.3 32-32l0-337.9c0-27.5-17.6-52-43.8-60.7L303.2 5.1c-9.9-3.3-20.5-3.3-30.4 0L43.8 81.4C17.6 90.1 0 114.6 0 142.1zM464 256l-352 0 0 64 352 0 0-64zM112 416l352 0 0-64-352 0 0 64zm352 32l-352 0 0 64 352 0 0-64z"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 860 B

After

Width:  |  Height:  |  Size: 860 B

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

View File

Before

Width:  |  Height:  |  Size: 860 KiB

After

Width:  |  Height:  |  Size: 860 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,68 @@
import { useState, useEffect } from "react";
import EmployeeList from "./EmployeeList";
import { useAllEmployees } from "../../hooks/useEmployees";
import showToast from "../../services/toastService";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import { useAssignEmpToBucket } from "../../hooks/useDirectory";
const AssignedBucket = ({ selectedBucket, handleClose }) => {
const { employeesList } = useAllEmployees(false);
const [selectedEmployees, setSelectedEmployees] = useState([]);
useEffect(() => {
if (selectedBucket) {
const preselected = employeesList
.filter((emp) => selectedBucket?.employeeIds?.includes(emp.employeeId))
.map((emp) => ({ ...emp, isActive: true }));
setSelectedEmployees(preselected);
}
}, [selectedBucket, employeesList]);
const { mutate: AssignEmployee, isPending } = useAssignEmpToBucket(() =>
handleClose()
);
const handleSubmit = async (e) => {
e.preventDefault();
const existingEmployeeIds = selectedBucket?.employeeIds || [];
const employeesToUpdate = selectedEmployees.filter((emp) => {
const isExisting = existingEmployeeIds.includes(emp.employeeId);
return (!isExisting && emp.isActive) || (isExisting && !emp.isActive);
});
if (employeesToUpdate.length === 0) {
showToast("No changes to update", "info");
return;
}
AssignEmployee({
bucketId: selectedBucket.id,
EmployeePayload: employeesToUpdate.map((emp) => ({
employeeId: emp.employeeId,
isActive: emp.isActive,
})),
});
};
return (
<form onSubmit={handleSubmit}>
<EmployeeList
employees={employeesList}
bucket={selectedBucket}
selectedEmployees={selectedEmployees}
onChange={setSelectedEmployees}
/>
<div className="mt-3 d-flex justify-content-end gap-3">
<button type="submit" className="btn btn-sm btn-primary" disabled={isPending}>
{isPending ? "Please Wait...":"Assign"}
</button>
</div>
</form>
);
};
export default AssignedBucket;

View File

@ -0,0 +1,95 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { bucketScheam } from "./DirectorySchema";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../common/Label";
const BucketForm = ({ selectedBucket, mode, onSubmit, onCancel, isPending }) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(bucketScheam),
defaultValues: selectedBucket || { name: "", description: "" },
});
useEffect(() => {
reset(selectedBucket || { name: "", description: "" });
}, [selectedBucket, reset]);
const isEditMode = mode === "edit";
const isCreateMode = mode === "create";
return (
<div className="row">
<div className="d-flex justify-content-between align-items-center">
<i
className="bx bx-left-arrow-alt bx-md cursor-pointer"
onClick={onCancel}
></i>
{/* Show edit toggle only for existing bucket in edit mode */}
{/* {isEditMode && (
<i className="bx bx-edit bx-sm text-primary cursor-pointer"></i>
)} */}
</div>
{(isCreateMode || isEditMode) ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3 mt-5">
<Label htmlFor="Name" className="text-start" required>
Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="mb-3">
<Label htmlFor="description" className="text-start" required>
Description
</Label>
<textarea
className="form-control form-control-sm"
{...register("description")}
rows="3"
/>
{errors.description && (
<small className="danger-text">{errors.description.message}</small>
)}
</div>
<div className="mt-4 mb-3 d-flex gap-3 justify-content-end">
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={onCancel}
disabled={isPending}
>
Cancel
</button>
<button type="submit" className="btn btn-sm btn-primary" disabled={isPending}>
{isPending ? "Please Wait " : isEditMode ? "Update" : "Create"}
</button>
</div>
</form>
) : (
<dl className="row text-start my-2">
<dt className="col-sm-2">Name</dt>
<dd className="col-sm-10">{selectedBucket?.name || "-"}</dd>
<dt className="col-sm-2">Description</dt>
<dd className="col-sm-10">{selectedBucket?.description || "-"}</dd>
</dl>
)}
</div>
);
};
export default BucketForm;

View File

@ -0,0 +1,53 @@
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProfile } from "../../hooks/useProfile";
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER } from "../../utils/constants";
const BucketList = ({ buckets, loading, searchTerm, onEdit, onDelete }) => {
const { profile } = useProfile();
const IsDirecrory_Admin = useHasUserPermission(DIRECTORY_ADMIN);
const IsDirectory_Manager = useHasUserPermission(DIRECTORY_MANAGER);
const sorted = buckets.filter((bucket) =>
bucket.name.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) return <div>Loading...</div>;
if (!loading && sorted.length === 0) return <div>No buckets found</div>;
return (
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 pt-3 px-2 px-sm-0">
{sorted.map((bucket) => (
<div className="col" key={bucket.id}>
<div className="card h-100">
<div className="card-body p-4">
<h6 className="card-title d-flex justify-content-between">
<span>{bucket.name}</span>
{(IsDirecrory_Admin ||
IsDirectory_Manager ||
bucket?.createdBy?.id === profile?.employeeInfo?.id) && (
<div className="d-flex gap-2">
<i
className="bx bx-edit bx-sm text-primary cursor-pointer"
onClick={() => onEdit(bucket)}
/>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => onDelete(bucket?.id)}
/>
</div>
)}
</h6>
<h6 className="card-subtitle mb-2 text-muted">
Contacts: {bucket.numberOfContacts || 0}
</h6>
<p className="card-text">
{bucket.description || "No description"}
</p>
</div>
</div>
</div>
))}
</div>
);
};
export default BucketList;

View File

@ -0,0 +1,217 @@
import React, { useState } from "react";
import Avatar from "../common/Avatar";
import { getBucketNameById } from "./DirectoryUtils";
import { useActiveInActiveContact, useBuckets } from "../../hooks/useDirectory";
import { getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
import ConfirmModal from "../common/ConfirmModal";
const CardViewContact = ({
IsActive,
contact,
setSelectedContact,
setIsOpenModal,
setOpen_contact,
setIsOpenModalNote,
IsDeleted,
restore,
}) => {
const { data, setManageContact, setContactOpen } = useDirectoryContext();
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { mutate: ActiveInActive, isPending } = useActiveInActiveContact();
const handleActiveInactive = (contactId) => {
ActiveInActive({ contactId, contactStatus: !IsActive });
};
return (
<>
<ConfirmModal
type="delete"
header="Delete Contact"
message="Are you sure you want delete?"
onSubmit={handleActiveInactive}
onClose={() => setIsDeleteModalOpen(false)}
loading={isPending}
paramData={contact.id}
isOpen={IsDeleteModalOpen}
/>
<div
className="card text-start border-1"
style={{ background: `${!IsActive ? "#f8f6f6" : ""}` }}
>
<div className="card-body px-1 py-2 pb-0">
<div className="d-flex justify-content-between">
<div
className={`d-flex align-items-center ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setContactOpen({ contact: contact, Open: true });
}
}}
>
<Avatar
size="xs"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>{" "}
<span className="text-heading fs-6"> {contact?.name}</span>
</div>
<div>
{IsActive && (
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded text-muted p-0"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() =>
setManageContact({
isOpen: true,
contactId: contact?.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit bx-xs text-primary me-2"></i>
<span className="align-left ">Modify</span>
</a>
</li>
<li>
<a
className="dropdown-item px-2 cursor-pointer py-1"
onClick={() => setIsDeleteModalOpen(true)}
>
<i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">Delete</span>
</a>
</li>
</ul>
</div>
)}
{!IsActive && (
<i
className={`bx ${
isPending ? "bx-loader-alt bx-spin" : "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => handleActiveInactive(contact.id)}
></i>
)}
</div>
</div>
<ul className="list-inline m-0 ps-4 d-flex align-items-start">
<li className="list-inline-item text-break small px-1 ms-5">
{contact?.organization}
</li>
</ul>
</div>
<div
className={`card-footer text-start px-9 py-1 ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<hr className="my-0" />
{contact?.designation && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i className="fa-solid fa-id-badge ms-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.designation}
</li>
</ul>
)}
{contact.contactEmails[0] && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i className="bx bx-envelope bx-xs mt-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.contactEmails[0].emailAddress}
</li>
</ul>
)}
{contact.contactPhones[0] && (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-1">
<i
className={` ${getPhoneIcon(
contact.contactPhones[0].label
)} bx-xs`}
></i>
</li>
<li className="list-inline-item text-small">
{contact.contactPhones[0]?.phoneNumber}
</li>
</ul>
)}
{contact?.tags?.length > 0 ? (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
{contact.tags.map((tag, index) => (
<li key={index} className="list-inline-item text-small active">
{tag.name}
</li>
))}
</ul>
) : (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
<li className="list-inline-item text-small active">Other</li>
</ul>
)}
<ul className="list-inline m-0 ms-2">
{contact?.bucketIds?.map((bucketId) => (
<li key={bucketId} className="list-inline-item me-1">
<span
className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1"
style={{ padding: "0.1rem 0.3rem" }}
>
<i className="bx bx-pin bx-xs"></i>
<span className="small-text">
{getBucketNameById(data, bucketId)}
</span>
</span>
</li>
))}
</ul>
</div>
</div>
</>
);
};
export default CardViewContact;

View File

@ -1,205 +0,0 @@
import React from "react";
import Avatar from "../common/Avatar";
import { getBucketNameById } from "./DirectoryUtils";
import { useBuckets } from "../../hooks/useDirectory";
import { getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
const CardViewDirectory = ({
IsActive,
contact,
setSelectedContact,
setIsOpenModal,
setOpen_contact,
setIsOpenModalNote,
IsDeleted,
restore,
}) => {
const { buckets } = useBuckets();
const { dirActions, setDirActions } = useDir();
return (
<div
className="card text-start border-1"
style={{ background: `${!IsActive ? "#f8f6f6" : ""}` }}
>
<div className="card-body px-1 py-2 pb-0">
<div className="d-flex justify-content-between">
<div
className={`d-flex align-items-center ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<Avatar
size="xs"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>{" "}
<span className="text-heading fs-6"> {contact?.name}</span>
</div>
<div>
{IsActive && (
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded text-muted p-0"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
<li
onClick={() => {
setSelectedContact(contact);
setIsOpenModal(true);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit bx-xs text-primary me-2"></i>
<span className="align-left ">Modify</span>
</a>
</li>
<li>
<a
className="dropdown-item px-2 cursor-pointer py-1"
onClick={() => IsDeleted(contact?.id)}
>
<i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">Delete</span>
</a>
</li>
</ul>
</div>
)}
{!IsActive && (
<i
className={`bx ${
dirActions.action && dirActions.id === contact.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setDirActions({ action: false, id: contact.id });
restore(contact.id);
}}
></i>
)}
</div>
</div>
<ul className="list-inline m-0 ps-4 d-flex align-items-start">
{/* <li className="list-inline-item me-1 small">
<i className="fa-solid fa-briefcase me-2"></i>
</li> */}
<li className="list-inline-item text-break small px-1 ms-5">
{contact.organization}
</li>
</ul>
</div>
<div
className={`card-footer text-start px-9 py-1 ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<hr className="my-0" />
{contact.designation && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i className="fa-solid fa-id-badge ms-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.designation}
</li>
</ul>
)}
{contact.contactEmails[0] && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i className="bx bx-envelope bx-xs mt-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.contactEmails[0].emailAddress}
</li>
</ul>
)}
{contact.contactPhones[0] && (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-1">
<i
className={` ${getPhoneIcon(
contact.contactPhones[0].label
)} bx-xs`}
></i>
</li>
<li className="list-inline-item text-small">
{contact.contactPhones[0]?.phoneNumber}
</li>
</ul>
)}
{contact?.tags?.length > 0 ? (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
{contact.tags.map((tag, index) => (
<li key={index} className="list-inline-item text-small active">
{tag.name}
</li>
))}
</ul>
) : (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
<li className="list-inline-item text-small active">Other</li>
</ul>
)}
<ul className="list-inline m-0 ms-2">
{contact?.bucketIds?.map((bucketId) => (
<li key={bucketId} className="list-inline-item me-1">
<span
className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1"
style={{ padding: "0.1rem 0.3rem" }}
>
<i className="bx bx-pin bx-xs"></i>
<span className="small-text">
{getBucketNameById(buckets, bucketId)}
</span>
</span>
</li>
))}
</ul>
</div>
</div>
);
};
export default CardViewDirectory;

View File

@ -1,18 +1,16 @@
import React, { useEffect, useState } from "react";
import { useContactProfile } from "../../hooks/useDirectory";
import React, { useState } from "react";
import { useContactNotes, useContactProfile1 } from "../../hooks/useDirectory";
import { ContactProfileSkeleton } from "./DirectoryPageSkeleton";
import Avatar from "../common/Avatar";
import moment from "moment";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import NotesDirectory from "./NotesDirectory";
const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
const { contactProfile, loading, refetch } = useContactProfile(contact?.id);
const ContactProfile = ({ contactId }) => {
const { data, isError, isLoading, error } = useContactProfile1(contactId.id);
const [copiedIndex, setCopiedIndex] = useState(null);
const [profileContactState, setProfileContactState] = useState(null);
const [expanded, setExpanded] = useState(false);
// Safely access description, defaulting to an empty string if not present
const description = profileContactState?.description || "";
const description = data?.description || "";
const limit = 500;
const toggleReadMore = () => setExpanded(!expanded);
@ -21,78 +19,32 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
const displayText = expanded
? description
: description.slice(0, limit) + (isLong ? "..." : "");
useEffect(() => {
if (contactProfile) {
const names = (contact?.name || "").trim().split(" ");
let firstName = "";
let middleName = "";
let lastName = "";
let fullName = contact?.name || "";
// Logic to determine first, middle, and last names
if (names.length === 1) {
firstName = names[0];
} else if (names.length === 2) {
firstName = names[0];
lastName = names[1];
} else if (names.length >= 3) {
firstName = names[0];
middleName = names[1]; // This was an error in the original prompt, corrected to names[1]
lastName = names[names.length - 1];
// Reconstruct full name to be precise with spacing
fullName = `${firstName} ${middleName ? middleName + " " : ""}${lastName}`;
} else {
// Fallback if no names or empty string
firstName = "Contact";
fullName = "Contact";
}
setProfileContactState({
...contactProfile,
firstName: contactProfile.firstName || firstName,
// Adding middleName and lastName to the state for potential future use or more granular access
middleName: contactProfile.middleName || middleName,
lastName: contactProfile.lastName || lastName,
fullName: contactProfile.fullName || fullName, // Prioritize fetched fullName, fallback to derived
});
}
}, [contactProfile, contact?.name]);
const handleCopy = (email, index) => {
navigator.clipboard.writeText(email);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
if (isError) return <div>{error.message}</div>;
if (isLoading) return <ContactProfileSkeleton />;
return (
<div className="p-1">
<div className="text-center m-0 p-0">
<p className="fw-semibold fs-6 m-0">Contact Profile</p>
<p className="fw-semibold fs-5 m-0">Contact Profile</p>
</div>
<div>
<div className="d-flex align-items-center mb-2">
<Avatar
size="sm"
classAvatar="m-0"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
firstName={(data?.name || "").trim().split(" ")[0]?.charAt(0) || ""}
lastName={(data?.name || "").trim().split(" ")[1]?.charAt(0) || ""}
/>
<div className="d-flex flex-column text-start ms-1">
<span className="m-0 fw-semibold">{contact?.name}</span>
<span className="m-0 fw-semibold">{data?.name}</span>
<small className="text-secondary small-text">
{profileContactState?.designation}
{data?.designation}
</small>
</div>
</div>
<div className="row ms-9">
<div className="col-12 col-md-6 d-flex flex-column text-start">
{profileContactState?.contactEmails?.length > 0 && (
{data?.contactEmails?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div
className="d-flex align-items-start"
@ -107,13 +59,15 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div style={{ flex: 1 }}>
<ul className="list-unstyled mb-0">
{profileContactState.contactEmails.map((email, idx) => (
{data.contactEmails.map((email, idx) => (
<li className="d-flex align-items-center mb-1" key={idx}>
<span className="me-1 text-break overflow-wrap">
{email.emailAddress}
</span>
<i
className={`bx bx-copy-alt cursor-pointer bx-xs text-start ${copiedIndex === idx ? "text-secondary" : "text-primary"
className={`bx bx-copy-alt cursor-pointer bx-xs text-start ${copiedIndex === idx
? "text-secondary"
: "text-primary"
}`}
title={copiedIndex === idx ? "Copied!" : "Copy Email"}
style={{ flexShrink: 0 }}
@ -126,7 +80,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div>
)}
{profileContactState?.contactPhones?.length > 0 && (
{data?.contactPhones?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -138,10 +92,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div>
<ul className="list-inline mb-0">
{profileContactState.contactPhones.map((phone, idx) => (
{data.contactPhones.map((phone, idx) => (
<li className="list-inline-item me-1" key={idx}>
{phone.phoneNumber}
{idx < profileContactState.contactPhones.length - 1 && ","}
{idx < data.contactPhones.length - 1 && ","}
</li>
))}
</ul>
@ -149,7 +103,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div>
)}
{profileContactState?.createdAt && (
{data?.createdAt && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -160,14 +114,12 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div>
<div className="d-flex align-items-center">
<span>
{moment(profileContactState.createdAt).format("DD MMMM, YYYY")}
</span>
<span>{formatUTCToLocalTime(data.createdAt)}</span>
</div>
</div>
)}
{profileContactState?.address && (
{data?.address && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-start">
@ -177,14 +129,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<span style={{ marginLeft: "26px" }}>:</span>
</div>
<div>
<span className="text-break small">{profileContactState.address}</span>
<span className="text-break small">{data.address}</span>
</div>
</div>
)}
</div>
<div className="col-12 col-md-6 d-flex flex-column text-start">
{profileContactState?.organization && (
{data?.organization && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -196,13 +148,13 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div className="d-flex align-items-center">
<span style={{ wordBreak: "break-word" }}>
{profileContactState.organization}
{data.organization}
</span>
</div>
</div>
)}
{profileContactState?.contactCategory && (
{data?.contactCategory && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -215,14 +167,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div>
<ul className="list-inline mb-0">
<li className="list-inline-item">
{profileContactState.contactCategory.name}
{data.contactCategory.name}
</li>
</ul>
</div>
</div>
)}
{profileContactState?.tags?.length > 0 && (
{data?.tags?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -234,7 +186,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div>
<ul className="list-inline mb-0">
{profileContactState.tags.map((tag, index) => (
{data.tags.map((tag, index) => (
<li key={index} className="list-inline-item">
{tag.name}
</li>
@ -244,7 +196,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div>
)}
{profileContactState?.buckets?.length > 0 && (
{data?.buckets?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -256,7 +208,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div>
<ul className="list-inline mb-0">
{profileContactState.buckets.map((bucket) => (
{data.buckets.map((bucket) => (
<li className="list-inline-item me-2" key={bucket.id}>
<span className="badge bg-label-primary my-1">
{bucket.name}
@ -269,7 +221,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
)}
</div>
{profileContactState?.projects?.length > 0 && (
{data?.projects?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div className="d-flex" style={{ minWidth: "130px" }}>
<span className="d-flex align-items-center">
@ -280,11 +232,11 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
</div>
<div className="text-start">
<ul className="list-inline mb-0">
{profileContactState.projects.map((project, index) => (
<ul className="list-inline text-wrap mb-0">
{data.projects.map((project, index) => (
<li className="list-inline-item me-2" key={project.id}>
{project.name}
{index < profileContactState.projects.length - 1 && ","}
{index < data.projects.length - 1 && ","}
</li>
))}
</ul>
@ -324,15 +276,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
)}
<hr className="my-1" />
<NotesDirectory
refetchProfile={refetch}
isLoading={loading}
contactProfile={profileContactState}
setProfileContact={setProfileContactState}
/>
<NotesDirectory contactId={data?.id} contactPerson={data?.name} />
</div>
</div>
);
};
export default ProfileContactDirectory;
export default ContactProfile;

View File

@ -0,0 +1,275 @@
import React from "react";
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
borderRadius: "4px",
}}
></div>
);
export const NoteCardSkeleton = () => {
return (
<div className="mt-5">
{Array.from({ length: 3 }).map((_, idx) => (
<div key={idx} className="card shadow-sm border-1 mb-3 p-3 rounded">
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<div
className="skeleton rounded-circle me-2"
style={{ width: 32, height: 32 }}
></div>
<div>
<SkeletonLine height={10} width="150px" />
<SkeletonLine height={10} width="100px" />
</div>
</div>
{/* Action Icons */}
<div className="d-flex gap-2">
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
></div>
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
></div>
</div>
</div>
<hr className="mt-0 mb-2" />
{/* Note Content */}
<div className="mx-4 px-2 text-start">
<SkeletonLine height={16} width="90%" />
<SkeletonLine height={16} width="80%" />
</div>
</div>
))}
</div>
);
};
export const MainDirectoryPageSkeleton = () => {
return (
<div className="container-fluid">
<div className="mt-5 card shadow-sm border-1 mb-3 p-3 rounded">
{/* Tabs & Export */}
<div className="d-flex justify-content-between align-items-center mb-3 px-2">
<div className="d-flex gap-3">
<SkeletonLine height={30} width="80px" />
<SkeletonLine height={30} width="90px" />
</div>
<SkeletonLine height={30} width="100px" />
</div>
{/* Search / Controls */}
</div>
<NoteCardSkeleton />
</div>
);
};
// 32702.75
// Skeleton for ListViewContact
export const ListViewContactSkeleton = ({ rows = 5 }) => {
const columns = ["Name", "Email", "Organization", "Category", "Action"];
return (
<div className="mt-5">
<div className="card">
<div className="card-datatable table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="shadow-sm">
{columns.map((col) => (
<th key={col} className="text-center">
<SkeletonLine height={20} width="80px" />
</th>
))}
</tr>
</thead>
<tbody className="px-2">
{Array.from({ length: rows }).map((_, idx) => (
<tr key={idx}>
{/* Name / Avatar */}
<td className="px-2 py-3">
<div className="d-flex align-items-center gap-2">
<div
className="skeleton rounded-circle"
style={{ width: 32, height: 32 }}
></div>
<SkeletonLine height={12} width="100px" />
</div>
</td>
{/* Email */}
<td className="px-2 py-3">
<SkeletonLine height={12} width="120px" />
</td>
{/* Organization */}
<td className="px-2 py-3">
<SkeletonLine height={12} width="120px" />
</td>
{/* Category */}
<td className="px-2 py-3">
<SkeletonLine height={12} width="100px" />
</td>
{/* Actions */}
<td className="px-2 py-3 text-center">
<div className="d-flex justify-content-center gap-2">
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
></div>
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
></div>
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
></div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export const CardViewContactSkeleton = ({ rows = 6 }) => {
return (
<div className="row mt-3">
{Array.from({ length: rows }).map((_, idx) => (
<div key={idx} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
<div className="card text-start border-1 h-100">
{/* Header */}
<div className="card-body px-2 py-2">
<div className="d-flex justify-content-between align-items-center mb-2">
<div className="d-flex align-items-center gap-2">
<div
className="skeleton rounded-circle"
style={{ width: 32, height: 32 }}
/>
<SkeletonLine height={14} width="120px" />
</div>
<div className="d-flex gap-2">
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
/>
<div
className="skeleton rounded"
style={{ width: 20, height: 20 }}
/>
</div>
</div>
<SkeletonLine height={12} width="150px" />
</div>
{/* Footer */}
<div className="card-footer px-3 py-2">
<SkeletonLine height={12} width="80%" className="mb-1" />
<SkeletonLine height={12} width="60%" className="mb-1" />
<SkeletonLine height={12} width="70%" className="mb-1" />
<div className="d-flex gap-1 mt-1">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="skeleton rounded"
style={{ width: 50, height: 20 }}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
);
};
export const ContactProfileSkeleton = () => {
return (
<div className="p-1">
{/* Header */}
<div className="text-center m-0 p-0 mb-3">
<SkeletonLine width="120px" height={20} className="mx-auto" />
</div>
{/* Avatar and Name */}
<div className="d-flex align-items-center mb-3">
<div className="skeleton rounded-circle" style={{ width: 40, height: 40 }} />
<div className="d-flex flex-column text-start ms-2">
<SkeletonLine width="120px" height={14} />
<SkeletonLine width="80px" height={12} className="mt-1" />
</div>
</div>
{/* Two-column details */}
<div className="row ms-9">
<div className="col-12 col-md-6 d-flex flex-column text-start">
{Array.from({ length: 5 }).map((_, idx) => (
<div key={idx} className="d-flex mb-2 align-items-start">
<SkeletonLine width="100px" height={12} className="me-2" />
<SkeletonLine width="150px" height={12} />
</div>
))}
</div>
<div className="col-12 col-md-6 d-flex flex-column text-start">
{Array.from({ length: 5 }).map((_, idx) => (
<div key={idx} className="d-flex mb-2 align-items-start">
<SkeletonLine width="100px" height={12} className="me-2" />
<SkeletonLine width="150px" height={12} />
</div>
))}
</div>
</div>
{/* Projects */}
{/* Description */}
<div className="d-flex mb-2 align-items-start" style={{ marginLeft: "3rem" }}>
<SkeletonLine width="100px" height={12} className="me-2" />
<SkeletonLine width="100%" height={50} />
</div>
<hr className="my-1" />
{/* Notes Section */}
{Array.from({ length: 3 }).map((_, idx) => (
<div key={idx} className="mb-2">
<SkeletonLine width="100%" height={60} className="mb-1" />
</div>
))}
</div>
);
};
export const NoetCard =({cards = 2})=>{
return(
<div className="row">
{Array.from({ length: cards }).map((_, idx) => (
<div key={idx} className="mb-2">
<SkeletonLine width="100%" height={60} className="mb-1" />
</div>
))}
</div>
)
}

View File

@ -1,6 +1,5 @@
import { z } from "zod";
export const ContactSchema = z
.object({
import { array, z } from "zod";
export const ContactSchema = z.object({
name: z.string().min(1, "Name is required"),
organization: z.string().min(1, "Organization name is required"),
contactCategoryId: z.string().nullable().optional(),
@ -26,7 +25,8 @@ export const ContactSchema = z
.string()
.min(6, "Invalid Number")
.max(13, "Invalid Number")
.regex(/^[\d\s+()-]+$/, "Invalid phone number format").or(z.literal("")),
.regex(/^[\d\s+()-]+$/, "Invalid phone number format")
.or(z.literal("")),
})
)
.optional()
@ -35,13 +35,15 @@ export const ContactSchema = z
tags: z
.array(
z.object({
id: z.string().nullable(),
id: z.string().nullable().optional(),
name: z.string(),
})
)
.min(1, { message: "At least one tag is required" }),
bucketIds: z.array(z.string()).nonempty({ message: "At least one bucket is required" })
})
bucketIds: z
.array(z.string())
.nonempty({ message: "At least one bucket is required" }),
});
// .refine((data) => {
// const hasValidEmail = (data.contactEmails || []).some(
@ -57,10 +59,43 @@ bucketIds: z.array(z.string()).nonempty({ message: "At least one bucket is requi
// path: ["contactPhone"],
// });
// Buckets
export const bucketScheam = z.object({
name: z.string().min(1, { message: "Name is required" }),
description:z.string().min(1,{message:"Description is required"})
})
description: z.string().min(1, { message: "Description is required" }),
});
export const defaultContactValue = {
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
};
export const contactsFilter = z.object({
bucketIds: z.array(z.string()).optional(),
categoryIds: z.array(z.string()).optional(),
});
export const defaultContactFilter = {
bucketIds: [],
categoryIds: [],
};
export const notesFilter = z.object({
createdByIds: z.array(z.string()).optional(),
organizations: z.array(z.string()).optional(),
});
export const defaultNotesFilter = {
createdByIds: [],
organizations: [],
};

View File

@ -21,6 +21,6 @@ export const getPhoneIcon = (type) => {
export const getBucketNameById = (buckets, id) => {
const bucket = buckets.find(b => b.id === id);
const bucket = buckets?.find(b => b.id === id);
return bucket ? bucket.name : 'Unknown';
};

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useSortableData } from "../../hooks/useSortableData";
import Avatar from "../common/Avatar";
const EmployeeList = ({ employees, onChange, bucket }) => {
const [employeefiltered, setEmployeeFilter] = useState([]);
const [employeeStatusList, setEmployeeStatusList] = useState([]);
@ -69,6 +70,8 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
`${employee?.firstName} ${employee?.lastName}`?.toLowerCase();
return fullName.includes(searchTerm.toLowerCase());
});
return (
<>
<div className="d-flex justify-content-between align-items-center mt-2">

View File

@ -0,0 +1,203 @@
import React, { useState } from "react";
import Avatar from "../common/Avatar";
import Pagination from "../common/Pagination";
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
import { useActiveInActiveContact } from "../../hooks/useDirectory";
import ConfirmModal from "../common/ConfirmModal";
const ListViewContact = ({ data, Pagination }) => {
const { showActive, setManageContact, setContactOpen } =
useDirectoryContext();
const [deleteContact, setDeleteContact] = useState({
contactId: null,
Open: false,
});
const [activeContact, setActiveContact] = useState(null);
const contactList = [
{
key: "name",
label: "Name",
getValue: (e) => (
<div className="d-flex align-items-center ps-1">
<Avatar
size="xs"
classAvatar="m-0"
firstName={(e?.name || "").trim().split(" ")[0] || ""}
lastName={(e?.name || "").trim().split(" ")[1] || ""}
/>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{e?.name || "N/A"}
</span>
</div>
),
align: "text-center",
},
{
key: "email",
label: "Email",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{e?.contactEmails?.[0]?.emailAddress || "N/A"}
</span>
),
align: "text-start",
},
{
key: "organization",
label: "Organization",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{e?.organization || "N/A"}
</span>
),
align: "text-start",
},
{
key: "category",
label: "Category",
getValue: (e) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "150px" }}
>
{e?.contactCategory?.name || "N/A"}
</span>
),
align: "text-start",
},
];
const { mutate: ActiveInActive, isPending } = useActiveInActiveContact(() =>
setDeleteContact({ contactId: null, Open: false })
);
const handleActiveInactive = (contactId) => {
ActiveInActive({ contactId: contactId, contactStatus: !showActive });
};
return (
<>
<ConfirmModal
type="delete"
header="Delete Contact"
message="Are you sure you want delete?"
onSubmit={handleActiveInactive}
onClose={() => setDeleteContact({ contactId: null, Open: false })}
loading={isPending}
paramData={deleteContact.contactId}
isOpen={deleteContact.Open}
/>
<div className="card ">
<div
className="card-datatable table-responsive"
id="horizontal-example"
>
<div className="dataTables_wrapper no-footer ">
<table className="table dataTable text-nowrap">
<thead>
<tr className="shadow-sm ">
{contactList?.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
<tbody>
{Array.isArray(data) && data.length > 0 ? (
data.map((row, i) => (
<tr
key={i}
style={{ background: `${!showActive ? "#f8f6f6" : ""}` }}
>
{contactList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
<td className="text-center">
{showActive ? (
<div className="d-flex justify-content-center gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
setContactOpen({ contact: row, Open: true })
}
></i>
<i
className="bx bx-edit text-secondary cursor-pointer"
onClick={() =>
setManageContact({
isOpen: true,
contactId: row.id,
})
}
></i>
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() =>
setDeleteContact({
contactId: row.id,
Open: true,
})
}
></i>
</div>
) : (
<i
className={`bx ${
isPending && activeContact === row.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setActiveContact(row.id);
handleActiveInactive(row.id);
}}
></i>
)}
</td>
</tr>
))
) : (
<tr style={{ height: "200px" }}>
<td
colSpan={contactList.length + 1}
className="text-center align-middle"
>
No contacts found
</td>
</tr>
)}
</tbody>
</table>
{Pagination && (
<div className="d-flex justify-content-start">
{Pagination}
</div>
)}
</div>
</div>
</div>
</>
);
};
export default ListViewContact;

View File

@ -1,138 +0,0 @@
import React, { useEffect } from "react";
import Avatar from "../common/Avatar";
import { getEmailIcon, getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
const ListViewDirectory = ({
IsActive,
contact,
setSelectedContact,
setIsOpenModal,
setOpen_contact,
setIsOpenModalNote,
IsDeleted,
restore,
}) => {
const { dirActions, setDirActions } = useDir();
// Get the first email and phone number if they exist
const firstEmail = contact.contactEmails?.[0];
const firstPhone = contact.contactPhones?.[0];
return (
<tr className={!IsActive ? "bg-light" : ""}>
<td
className="text-start cursor-pointer"
style={{ width: "18%" }}
colSpan={2}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>
<span className="text-truncate mx-0" style={{ maxWidth: "150px" }}>
{contact?.name || ""}
</span>
</div>
</td>
<td className="px-2" style={{ width: "20%" }}>
<div className="d-flex flex-column align-items-start text-truncate">
{firstEmail ? (
<span key={firstEmail.id} className="text-truncate">
<i
className={getEmailIcon(firstEmail.label)}
style={{ fontSize: "12px" }}
></i>
<a
href={`mailto:${firstEmail.emailAddress}`}
className="text-decoration-none ms-1"
>
{firstEmail.emailAddress}
</a>
</span>
) : (
<span className="small-text m-0 px-2">NA</span>
)}
</div>
</td>
<td className="px-2" style={{ width: "20%" }}>
<div className="d-flex flex-column align-items-start text-truncate">
{firstPhone ? (
<span key={firstPhone.id}>
<i
className={getPhoneIcon(firstPhone.label)}
style={{ fontSize: "12px" }}
></i>
<span className="ms-1">{firstPhone.phoneNumber}</span>
</span>
) : (
<span className="text-small m-0 px-2">NA</span>
)}
</div>
</td>
<td
colSpan={2}
className="text-start text-truncate px-2"
style={{ width: "20%", maxWidth: "200px" }}
>
{contact.organization}
</td>
<td className="px-2" style={{ width: "10%" }}>
<span className="text-truncate">
{contact?.contactCategory?.name || "Other"}
</span>
</td>
<td className="align-middle text-center" style={{ width: "12%" }}>
{IsActive && (
<>
<i
className="bx bx-edit bx-sm text-primary cursor-pointer me-2"
onClick={() => {
setSelectedContact(contact);
setIsOpenModal(true);
}}
></i>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => IsDeleted(contact.id)}
></i>
</>
)}
{!IsActive && (
<i
className={`bx ${
dirActions.action && dirActions.id === contact.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setDirActions({ action: false, id: contact.id });
restore(contact.id);
}}
></i>
)}
</td>
</tr>
);
};
export default ListViewDirectory;

View File

@ -1,412 +0,0 @@
import React, { useEffect, useState } from "react";
import IconButton from "../common/IconButton";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { bucketScheam } from "./DirectorySchema";
import showToast from "../../services/toastService";
import Directory from "../../pages/Directory/Directory";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import { useBuckets } from "../../hooks/useDirectory";
import EmployeeList from "./EmployeeList";
import { useAllEmployees, useEmployees } from "../../hooks/useEmployees";
import { useSortableData } from "../../hooks/useSortableData";
import ConfirmModal from "../common/ConfirmModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER } from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
import Label from "../common/Label";
const ManageBucket = () => {
const { profile } = useProfile();
const [bucketList, setBucketList] = useState([]);
const { employeesList } = useAllEmployees(false);
const [selectedEmployee, setSelectEmployee] = useState([]);
const { buckets, loading, refetch } = useBuckets();
const [action_bucket, setAction_bucket] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [selected_bucket, select_bucket] = useState(null);
const [deleteBucket, setDeleteBucket] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const DirManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
const {
items: sortedBuckteList,
requestSort,
sortConfig,
} = useSortableData(bucketList, {
key: (e) => `${e.name}`,
direction: "asc",
});
const getSortIcon = () => {
if (!sortConfig) return null;
return sortConfig.direction === "asc" ? (
<i className="bx bx-caret-up text-secondary"></i>
) : (
<i className="bx bx-caret-down text-secondary"></i>
);
};
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(bucketScheam),
defaultValues: {
name: "",
description: "",
},
});
const onSubmit = async (data) => {
setSubmitting(true);
try {
const cache_buckets = getCachedData("buckets") || [];
let response;
const arraysAreEqual = (a, b) => {
if (a.length !== b.length) return false;
const setA = new Set(a);
const setB = new Set(b);
return [...setA].every((id) => setB.has(id));
};
if (selected_bucket) {
const payload = { ...data, id: selected_bucket.id };
response = await DirectoryRepository.UpdateBuckets(
selected_bucket.id,
payload
);
const updatedBuckets = cache_buckets.map((bucket) =>
bucket.id === selected_bucket.id ? response?.data : bucket
);
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
const existingEmployeeIds = selected_bucket?.employeeIds || [];
const employeesToUpdate = selectedEmployee.filter((emp) => {
const isExisting = existingEmployeeIds.includes(emp.employeeId);
return (!isExisting && emp.isActive) || (isExisting && !emp.isActive);
});
const newActiveEmployeeIds = selectedEmployee
.filter((emp) => {
const isExisting = existingEmployeeIds.includes(emp.employeeId);
return (
(!isExisting && emp.isActive) || (isExisting && !emp.isActive)
);
})
.map((emp) => emp.employeeId);
if (
!arraysAreEqual(newActiveEmployeeIds, existingEmployeeIds) &&
employeesToUpdate.length !== 0
) {
try {
response = await DirectoryRepository.AssignedBuckets(
selected_bucket.id,
employeesToUpdate
);
} catch (assignError) {
const assignMessage =
assignError?.response?.data?.message ||
assignError?.message ||
"Error assigning employees.";
showToast(assignMessage, "error");
}
}
const updatedData = cache_buckets?.map((bucket) =>
bucket.id === response?.data?.id ? response.data : bucket
);
cacheData("buckets", updatedData);
setBucketList(updatedData);
showToast("Bucket Updated Successfully", "success");
} else {
response = await DirectoryRepository.CreateBuckets(data);
const updatedBuckets = [...cache_buckets, response?.data];
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
showToast("Bucket Created Successfully", "success");
}
handleBack();
} catch (error) {
const message =
error?.response?.data?.message ||
error?.message ||
"Error occurred during API call";
showToast(message, "error");
} finally {
setSubmitting(false);
}
};
const handleDeleteContact = async () => {
try {
const resp = await DirectoryRepository.DeleteBucket(deleteBucket);
const cache_buckets = getCachedData("buckets") || [];
const updatedBuckets = cache_buckets.filter(
(bucket) => bucket.id !== deleteBucket
);
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
showToast("Bucket deleted successfully", "success");
setDeleteBucket(null);
} catch (error) {
const message =
error?.response?.data?.message ||
error?.message ||
"Error occurred during API call.";
showToast(message, "error");
}
};
useEffect(() => {
reset({
name: selected_bucket?.name || "",
description: selected_bucket?.description || "",
});
}, [selected_bucket]);
useEffect(() => {
setBucketList(buckets);
}, [buckets]);
const handleBack = () => {
select_bucket(null);
setAction_bucket(false);
setSubmitting(false);
reset({ name: "", description: "" });
setSelectEmployee([]);
};
const sortedBucktesList = sortedBuckteList?.filter((bucket) => {
const term = searchTerm?.toLowerCase();
const name = bucket.name?.toLowerCase();
return name?.includes(term);
});
return (
<>
{deleteBucket && (
<ConfirmModal
isOpen={!!deleteBucket}
type="delete"
header="Delete Bucket"
message="Are you sure you want to delete this bucket?"
onSubmit={handleDeleteContact}
onClose={() => setDeleteBucket(null)}
/>
)}
<div className="container m-0 p-0" style={{ minHeight: "00px" }}>
<div className="d-flex justify-content-center">
<p className="fs-5 fw-semibold m-0">Manage Buckets</p>
</div>
<div className="d-flex justify-content-between px-2 px-sm-0 mt-5 mt-3 align-items-center ">
{action_bucket ? (
<i
className={`fa-solid fa-arrow-left fs-5 cursor-pointer mb-5`}
onClick={handleBack}
></i>
) : (
<div className="d-flex align-items-center gap-2">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search Bucket ..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<i
className={`bx bx-refresh cursor-pointer fs-4 ${
loading ? "spin" : ""
}`}
title="Refresh"
onClick={() => refetch()}
/>
</div>
)}
<button
type="button"
className={`btn btn-sm btn-primary ms-auto ${
action_bucket ? "d-none" : ""
}`}
onClick={() => {
setAction_bucket(true);
select_bucket(null);
reset({ name: "", description: "" });
setSelectEmployee([]);
}}
>
<i className="bx bx-plus-circle me-2"></i>
Add Bucket
</button>
</div>
<div>
{!action_bucket ? (
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 pt-3 px-2 px-sm-0">
{loading && (
<div className="col-12">
<div
className="d-flex justify-content-center align-items-center py-5 w-100"
style={{ marginLeft: "250px" }}
>
Loading...
</div>
</div>
)}
{!loading && buckets.length === 0 && searchTerm.trim() === "" && (
<div className="col-12">
<div
className="d-flex justify-content-center align-items-center py-5 w-100"
style={{ marginLeft: "250px" }}
>
No buckets available.
</div>
</div>
)}
{!loading &&
buckets.length > 0 &&
sortedBucktesList.length === 0 && (
<div className="col-12">
<div
className="d-flex justify-content-center align-items-center py-5 w-100"
style={{ marginLeft: "250px" }}
>
No matching buckets found.
</div>
</div>
)}
{!loading &&
sortedBucktesList.map((bucket) => (
<div className="col" key={bucket.id}>
<div className="card h-100">
<div className="card-body p-4 justify-content-start">
<h6 className="card-title d-flex justify-content-between align-items-center">
<span>{bucket.name}</span>
{(DirManager ||
DirAdmin ||
bucket?.createdBy?.id ===
profile?.employeeInfo?.id) && (
<div className="d-flex gap-2">
<i
className="bx bx-edit bx-sm text-primary cursor-pointer"
onClick={() => {
select_bucket(bucket);
setAction_bucket(true);
const initialSelectedEmployees = employeesList
.filter((emp) =>
bucket.employeeIds?.includes(
emp.employeeId
)
)
.map((emp) => ({ ...emp, isActive: true }));
setSelectEmployee(initialSelectedEmployees);
}}
></i>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer ms-0"
onClick={() => setDeleteBucket(bucket?.id)}
></i>
</div>
)}
</h6>
<h6 className="card-subtitle mb-2 text-muted text-start">
Contacts:{" "}
{bucket.numberOfContacts
? bucket.numberOfContacts
: 0}
</h6>
<p
className="card-text text-start"
title={bucket.description}
>
{bucket.description || "No description available."}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<>
<form onSubmit={handleSubmit(onSubmit)} className="px-2 px-sm-0">
<div className="mb-3 text-start">
<Label htmlFor="bucketName" className="form-label" required>
Bucket Name
</Label>
<input
id="bucketName"
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="mb-3 text-start">
<Label htmlFor="bucketDescription" className="form-label" required>
Bucket Description
</Label>
<textarea
id="bucketDescription"
className="form-control form-control-sm"
rows="3"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">
{errors.description.message}
</small>
)}
</div>
{selected_bucket && (
<EmployeeList
employees={employeesList}
onChange={(data) => setSelectEmployee(data)}
bucket={selected_bucket}
/>
)}
<div className="mt-4 d-flex justify-content-end gap-3">
<button
type="button"
onClick={handleBack}
className="btn btn-sm btn-label-secondary"
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</>
)}
</div>
</div>
</>
);
};
export default ManageBucket;

View File

@ -0,0 +1,98 @@
import { useState } from "react";
import {
useBucketList,
useBuckets,
useCreateBucket,
useUpdateBucket,
} from "../../hooks/useDirectory";
import { useAllEmployees } from "../../hooks/useEmployees";
import BucketList from "./BucketList";
import BucketForm from "./BucketForm";
import AssignEmployees from "./AssignedBucket";
import AssignedBucket from "./AssignedBucket";
const ManageBucket1 = () => {
const { data, isError, isLoading, error } = useBucketList();
const { employeesList } = useAllEmployees(false);
const [action, setAction] = useState(null); // "create" | "edit" | null
const [selectedBucket, setSelectedBucket] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const handleClose = ()=>{
setAction(null);
setSelectedBucket(null);
}
const { mutate: createBucket, isPending: creating } = useCreateBucket(() => {
handleClose()
});
const { mutate: updateBucket, isPending: updating } = useUpdateBucket(() => {
handleClose()
});
const handleSubmit = (BucketPayload) => {
if (selectedBucket) {
updateBucket({
bucketId: selectedBucket.id,
BucketPayload: { ...BucketPayload, id: selectedBucket.id },
});
} else createBucket(BucketPayload);
};
return (
<div className="container m-0 p-0" style={{ minHeight: "00px" }}>
<div className="d-flex justify-content-center">
<p className="fs-5 fw-semibold m-0">Manage Buckets</p>
</div>
{action ? (
<>
<BucketForm
selectedBucket={selectedBucket}
mode={action} // pass create | edit
onSubmit={handleSubmit}
onCancel={() => {
setAction(null);
setSelectedBucket(null);
}}
isPending={creating || updating}
/>
{action === "edit" && selectedBucket && (
<AssignedBucket selectedBucket={selectedBucket} handleClose={handleClose} />
)}
</>
) : (
<>
<div className="d-flex justify-content-between align-items-center gap-2 my-2">
<input
type="search"
className="form-control form-control-sm w-25"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button
className="btn btn-sm btn-primary"
onClick={() => setAction("create")}
>
<i className="bx bx-plus-circle me-2"></i>
Add Bucket
</button>
</div>
<BucketList
buckets={data}
loading={isLoading}
searchTerm={searchTerm}
onEdit={(bucket) => {
setSelectedBucket(bucket);
setAction("edit");
}}
onDelete={(id) => console.log("delete", id)}
/>
</>
)}
</div>
);
};
export default ManageBucket1;

View File

@ -1,65 +1,49 @@
import React, { useEffect, useState } from "react";
import {
useForm,
useFieldArray,
FormProvider,
useFormContext,
} from "react-hook-form";
import { useForm, useFieldArray, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TagInput from "../common/TagInput";
import IconButton from "../common/IconButton";
import useMaster, {
import {
useBuckets,
useContactDetails,
useCreateContact,
useDesignation,
useOrganization,
useUpdateContact,
} from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import {
useContactCategory,
useContactTags,
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import {
useBuckets,
useDesignation,
useOrganization,
} from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
import { ContactSchema, defaultContactValue } from "./DirectorySchema";
import InputSuggestions from "../common/InputSuggestion";
import Label from "../common/Label";
const ManageDirectory = ({ submitContact, onCLosed }) => {
const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const [categoryData, setCategoryData] = useState([]);
const [TagsData, setTagsData] = useState([]);
const { data, loading } = useMaster();
const ManageContact = ({ contactId, closeModal }) => {
// fetch master data
const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { organizationList, loading: orgLoading } = useOrganization();
const { designationList, loading: designloading } = useDesignation();
const { contactTags, loading: Tagloading } = useContactTags();
const [IsSubmitting, setSubmitting] = useState(false);
const { organizationList } = useOrganization();
const { designationList } = useDesignation();
const { contactTags } = useContactTags();
// fetch contact details if editing
const { data: contactData, isLoading: isContactLoading } = useContactDetails(
contactId,
{
enabled: !!contactId,
}
);
const [showSuggestions, setShowSuggestions] = useState(false);
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
const dispatch = useDispatch();
const methods = useForm({
resolver: zodResolver(ContactSchema),
defaultValues: {
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
},
defaultValues: defaultContactValue,
});
const {
@ -67,7 +51,6 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
handleSubmit,
control,
getValues,
trigger,
setValue,
watch,
reset,
@ -87,38 +70,50 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
} = useFieldArray({ control, name: "contactPhones" });
useEffect(() => {
if (emailFields.length === 0) {
appendEmail({ label: "Work", emailAddress: "" });
if (contactId && contactData) {
reset({
name: contactData.name || "",
description: contactData.description || "",
designation: contactData.designation || "",
organization: contactData.organization || "",
contactEmails: contactData.contactEmails?.length
? contactData.contactEmails
: [{ label: "Work", emailAddress: "" }],
contactPhones: contactData.contactPhones?.length
? contactData.contactPhones
: [{ label: "Office", phoneNumber: "" }],
contactCategoryId: contactData.contactCategory?.id || "",
address: contactData?.address || "",
projectIds: contactData.projects?.map((p) => p.id) || [],
bucketIds: contactData.buckets?.map((b) => b.id) || [],
tags: contactData.tags || [],
});
}
if (phoneFields.length === 0) {
}, [contactId, contactData, reset]);
useEffect(() => {
if (!contactId) {
if (emailFields.length === 0)
appendEmail({ label: "Work", emailAddress: "" });
if (phoneFields.length === 0)
appendPhone({ label: "Office", phoneNumber: "" });
}
}, [emailFields.length, phoneFields.length]);
}, [
contactId,
emailFields.length,
phoneFields.length,
appendEmail,
appendPhone,
]);
const handleAddEmail = async () => {
const emails = getValues("contactEmails");
const lastIndex = emails.length - 1;
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
if (valid) {
appendEmail({ label: "Work", emailAddress: "" });
}
};
const watchBucketIds = watch("bucketIds") || [];
const handleAddPhone = async () => {
const phones = getValues("contactPhones");
const lastIndex = phones.length - 1;
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
if (valid) {
appendPhone({ label: "Office", phoneNumber: "" });
}
};
const watchBucketIds = watch("bucketIds");
// handle logic when input of desgination is changed
const handleDesignationChange = (e) => {
const val = e.target.value;
const matches = designationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
@ -127,36 +122,29 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
setTimeout(() => setShowSuggestions(false), 5000);
};
// handle logic when designation is selected
const handleSelectDesignation = (val) => {
setShowSuggestions(false);
setValue("designation", val);
};
// Handle phone number input to only allow numbers and max length of 10
const handlePhoneInput = (e) => {
const value = e.target.value.replace(/[^0-9]/g, "");
e.target.value = value.slice(0, 10);
};
const toggleBucketId = (id) => {
const updated = watchBucketIds?.includes(id)
? watchBucketIds.filter((val) => val !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
// bucket toggle
const handleCheckboxChange = (id) => {
const updated = watchBucketIds.includes(id)
? watchBucketIds.filter((i) => i !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
// create & update mutations
const { mutate: CreateContact, isPending: creating } = useCreateContact(() =>
handleClosed()
);
const { mutate: UpdateContact, isPending: updating } = useUpdateContact(() =>
handleClosed()
);
const onSubmit = (data) => {
const cleaned = {
const payload = {
...data,
contactEmails: (data.contactEmails || []).filter(
(e) => e.emailAddress?.trim() !== ""
@ -166,23 +154,45 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
),
};
setSubmitting(true);
submitContact(cleaned, reset, setSubmitting);
if (contactId) {
const contactPayload = { ...data, id: contactId };
UpdateContact({
contactId: contactData.id,
contactPayload: contactPayload,
});
} else {
CreateContact(payload);
}
};
const orgValue = watch("organization");
const handleClosed = () => {
onCLosed();
closeModal();
};
const handleAddEmail = () => {
appendEmail({ label: "Work", emailAddress: "" });
};
const handleAddPhone = () => {
appendPhone({ label: "Office", phoneNumber: "" });
};
const isPending = updating || creating;
return (
<FormProvider {...methods}>
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-center align-items-center">
<h5 className="m-0 fw-18"> Create New Contact</h5>
<div className="d-flex justify-content-center align-items-center mb-4">
<h5 className="m-0 fw-18">
{contactId ? "Edit Contact" : "Create New Contact"}
</h5>
</div>
{/* Name + Organization */}
<div className="row">
<div className="col-md-6 text-start">
<Label className="form-label" required>Name</Label>
<Label htmlFor={"name"} required>
Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
@ -191,20 +201,26 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="col-md-6 text-start">
<Label className="form-label" required>Organization</Label>
<Label htmlFor={"organization"} required>
Organization
</Label>
<InputSuggestions
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
value={watch("organization") || ""}
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
error={errors.organization?.message}
/>
</div>
</div>
{/* Designation */}
<div className="row mt-1">
<div className="col-md-6 text-start">
<Label className="form-label" required>Designation</Label>
<div className="col-md-6 text-start position-relative">
<Label htmlFor={"designation"} required>
Designation
</Label>
<input
className="form-control form-control-sm"
{...register("designation")}
@ -218,26 +234,14 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius: "0px",
}}
>
{filteredDesignationList.map((designation) => (
<li
key={designation}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
className="list-group-item list-group-item-action"
style={{ cursor: "pointer", fontSize: "14px" }}
onMouseDown={() => handleSelectDesignation(designation)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{designation}
</li>
@ -251,6 +255,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
)}
</div>
</div>
{/* Emails + Phones */}
<div className="row mt-1">
<div className="col-md-6">
{emailFields.map((field, index) => (
@ -268,11 +274,6 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
<option value="Personal">Personal</option>
<option value="Other">Other</option>
</select>
{errors.contactEmails?.[index]?.label && (
<small className="danger-text">
{errors.contactEmails[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Email</label>
@ -290,7 +291,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
/>
) : (
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-primary"
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removeEmail(index)}
/>
)}
@ -304,6 +305,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
</div>
))}
</div>
<div className="col-md-6">
{phoneFields.map((field, index) => (
<div
@ -320,22 +322,15 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
<option value="Personal">Personal</option>
<option value="Business">Business</option>
</select>
{errors.phone?.[index]?.label && (
<small className="danger-text">
{errors.ContactPhones[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Phone</label>
<div className="d-flex align-items-center">
<input
type="tel"
type="text"
className="form-control form-control-sm"
{...register(`contactPhones.${index}.phoneNumber`)}
placeholder="9876543210"
onInput={handlePhoneInput}
maxLength={10}
/>
{index === phoneFields.length - 1 ? (
<i
@ -344,7 +339,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
/>
) : (
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danager"
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removePhone(index)}
/>
)}
@ -358,11 +353,9 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
</div>
))}
</div>
{errors.contactPhone?.message && (
<div className="danger-text">{errors.contactPhone.message}</div>
)}
</div>
{/* Category + Projects */}
<div className="row my-1">
<div className="col-md-6 text-start">
<label className="form-label">Category</label>
@ -408,16 +401,23 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
</div>
</div>
{/* Tags */}
<div className="col-12 text-start">
<TagInput name="tags" label="Tags" options={contactTags} />
<TagInput
name="tags"
label="Tags"
options={contactTags}
isRequired={true}
/>
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
{/* Buckets */}
<div className="row">
<div className="col-md-12 mt-1 text-start">
<Label className="form-label" required>Select Bucket</Label>
<label className="form-label ">Select Bucket</label>
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
{bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => (
@ -444,13 +444,12 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
))}
</ul>
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
<small className="danger-text">{errors.bucketIds.message}</small>
)}
</div>
</div>
{/* Address + Description */}
<div className="col-12 text-start">
<label className="form-label">Address</label>
<textarea
@ -459,9 +458,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
{...register("address")}
/>
</div>
<div className="col-12 text-start mt-1">
<Label className="form-label" required>Description</Label>
<div className="col-12 text-start">
<label className="form-label">Description</label>
<textarea
className="form-control form-control-sm"
rows="2"
@ -477,11 +475,12 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
className="btn btn-sm btn-label-secondary"
type="button"
onClick={handleClosed}
disabled={isPending}
>
Cancel
</button>
<button className="btn btn-sm btn-primary" type="submit">
{IsSubmitting ? "Please Wait..." : "Submit"}
<button className="btn btn-sm btn-primary" type="submit" disabled={isPending}>
{isPending ? "Please Wait..." : "Submit"}
</button>
</div>
@ -490,4 +489,4 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
);
};
export default ManageDirectory;
export default ManageContact;

View File

@ -6,21 +6,15 @@ import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import showToast from "../../services/toastService";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import "../common/TextEditor/Editor.css";
import { useActiveInActiveNote } from "../../hooks/useDirectory";
const NoteCardDirectory = ({
refetchProfile,
refetchNotes,
noteItem,
contactId,
setProfileContact,
}) => {
const NoteCardDirectory = ({ noteItem, contactId }) => {
const [editing, setEditing] = useState(false);
const [editorValue, setEditorValue] = useState(noteItem.note);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isActivProcess, setActiveProcessing] = useState(false);
// State to manage hover status
const [isHovered, setIsHovered] = useState(false);
const handleUpdateNote = async () => {
@ -36,12 +30,6 @@ const NoteCardDirectory = ({
noteItem.id,
payload
);
setProfileContact((prev) => ({
...prev,
notes: prev.notes.map((note) =>
note.id === noteItem.id ? response?.data : note
),
}));
const cached_contactProfile = getCachedData("Contact Profile");
@ -81,10 +69,6 @@ const NoteCardDirectory = ({
noteItem.id,
activeStatue
);
setProfileContact((prev) => ({
...prev,
notes: prev.notes.filter((note) => note.id !== noteItem.id),
}));
const cachedContactProfile = getCachedData("Contact Profile");
@ -106,8 +90,6 @@ const NoteCardDirectory = ({
}
setIsDeleting(false);
setActiveProcessing(false);
refetchNotes(contactId, false);
refetchProfile(contactId);
showToast(
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
"success"
@ -121,6 +103,10 @@ const NoteCardDirectory = ({
showToast(msg, "error");
}
};
const { mutate: handleActiveInActive, isPending } = useActiveInActiveNote(
() => {}
);
return (
<div
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
@ -167,10 +153,15 @@ const NoteCardDirectory = ({
onClick={() => setEditing(true)}
></i>
{!isDeleting ? (
{!isPending ? (
<i
className="bx bx-trash bx-sm me-1 text-secondary cursor-pointer"
onClick={() => handleDeleteNote(!noteItem?.isActive)}
onClick={() =>
handleActiveInActive({
noteId: noteItem?.id,
noteStatus: !noteItem?.isActive,
})
}
></i>
) : (
<div
@ -181,12 +172,17 @@ const NoteCardDirectory = ({
</div>
)}
</>
) : isActivProcess ? (
) : isPending ? (
<i className="bx bx-loader-alt bx-spin text-primary"></i>
) : (
<i
className="bx bx-recycle me-1 text-primary cursor-pointer"
onClick={() => handleDeleteNote(!noteItem?.isActive)}
onClick={() =>
handleActiveInActive({
noteId: noteItem?.id,
noteStatus: !noteItem?.isActive,
})
}
title="Restore"
></i>
)}

View File

@ -7,8 +7,8 @@ import showToast from "../../services/toastService";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
import "../common/TextEditor/Editor.css";
import ProfileContactDirectory from "./ProfileContactDirectory";
import GlobalModel from "../common/GlobalModel";
import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory";
const NoteCardDirectoryEditable = ({
noteItem,
@ -25,55 +25,24 @@ const NoteCardDirectoryEditable = ({
const [open_contact, setOpen_contact] = useState(null);
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
const { mutate: UpdateNote, isPending: isUpatingNote } = useUpdateNote(() =>
setEditing(false)
);
const { mutate: ActiveInactive, isPending: isUpdatingStatus } =
useActiveInActiveNote(() => setIsDeleteModalOpen(false));
const handleUpdateNote = async () => {
try {
setIsLoading(true);
const payload = {
id: noteItem.id,
note: editorValue,
contactId,
};
const response = await DirectoryRepository.UpdateNote(
noteItem.id,
payload
);
const cachedContactProfile = getCachedData("Contact Profile");
if (cachedContactProfile?.contactId === contactId) {
const updatedCache = {
...cachedContactProfile,
data: {
...cachedContactProfile.data,
notes: cachedContactProfile.data.notes.map((note) =>
note.id === noteItem.id ? response.data : note
),
},
};
cacheData("Contact Profile", updatedCache);
}
onNoteUpdate?.(response.data);
setEditing(false);
showToast("Note updated successfully", "success");
} catch (error) {
showToast("Failed to update note", "error");
} finally {
setIsLoading(false);
}
UpdateNote({ noteId: noteItem.id, notePayload: payload });
};
const suspendEmployee = async () => {
try {
setIsDeleting(true);
await DirectoryRepository.DeleteNote(noteItem.id, false);
onNoteDelete?.(noteItem.id);
setIsDeleteModalOpen(false);
showToast("Note deleted successfully", "success");
} catch (error) {
showToast("Failed to delete note", "error");
} finally {
setIsDeleting(false);
}
const ActiveInActive = (noteItem) => {
ActiveInactive({ noteId: noteItem.id, noteStatus: !noteItem.isActive });
};
const contactProfile = (contactId) => {
@ -98,24 +67,7 @@ const NoteCardDirectoryEditable = ({
return (
<>
{isOpenModalNote && (
<GlobalModel
isOpen={isOpenModalNote}
closeModal={() => {
setOpen_contact(null);
setIsOpenModalNote(false);
}}
size="xl"
>
{open_contact && (
<ProfileContactDirectory
contact={open_contact}
setOpen_contact={setOpen_contact}
closeModal={() => setIsOpenModalNote(false)}
/>
)}
</GlobalModel>
)}
<div
className="card shadow-sm border-1 mb-3 p-4 rounded"
style={{
@ -187,12 +139,12 @@ const NoteCardDirectoryEditable = ({
<div className="spinner-border spinner-border-sm text-danger" />
)}
</>
) : isRestoring ? (
) : isUpdatingStatus ? (
<i className="bx bx-loader-alt bx-spin text-primary"></i>
) : (
<i
className="bx bx-recycle me-2 text-primary cursor-pointer"
onClick={handleRestore}
onClick={() => ActiveInActive(noteItem)}
title="Restore"
></i>
)}
@ -210,20 +162,25 @@ const NoteCardDirectoryEditable = ({
theme="snow"
className="compact-editor"
/>
<div className="d-flex justify-content-end gap-3 mt-2">
<span
className="text-secondary cursor-pointer"
<div className="d-flex justify-content-end gap-0 mt-2">
<button
type="button"
className="btn btn-sm btn-label-secondary me-2 px-2 py-1"
onClick={() => setEditing(false)}
>
Cancel
</span>
<span
className="text-primary cursor-pointer"
</button>
<button
type="button"
className="btn btn-sm btn-primary px-2 py-1"
onClick={handleUpdateNote}
disabled={isUpatingNote}
>
{isLoading ? "Saving..." : "Submit"}
</span>
{isUpatingNote ? "Saving..." : "Submit"}
</button>
</div>
</>
) : (
<div
@ -233,19 +190,16 @@ const NoteCardDirectoryEditable = ({
)}
</div>
{/* Delete Confirm Modal */}
{isDeleteModalOpen && (
<ConfirmModal
isOpen={isDeleteModalOpen}
type="delete"
header="Delete Note"
message="Are you sure you want to delete this note?"
onSubmit={suspendEmployee}
onSubmit={ActiveInActive}
onClose={() => setIsDeleteModalOpen(false)}
loading={isDeleting}
loading={isUpdatingStatus}
paramData={noteItem}
isOpen={isDeleteModalOpen}
/>
)}
</>
);
};

View File

@ -9,8 +9,8 @@ const NotesCardViewDirectory = ({
searchText,
filterAppliedNotes,
}) => {
const projectId = useSelectedProject();
const projectId = useSelectedProject();
const [allNotes, setAllNotes] = useState([]);
const [filteredNotes, setFilteredNotes] = useState([]);
const [loading, setLoading] = useState(true);
@ -29,7 +29,7 @@ const NotesCardViewDirectory = ({
const fetchNotes = async (projId) => {
setLoading(true);
try {
const response = await DirectoryRepository.GetNotes(1000, 1, projId); // pass projectId
const response = await DirectoryRepository.GetNotes(1000, 1, projId);
const fetchedNotes = response.data?.data || [];
setAllNotes(fetchedNotes);
setNotesForFilter(fetchedNotes)
@ -116,11 +116,7 @@ const NotesCardViewDirectory = ({
return (
<div className="w-100 h-100 ">
{/* Filter Dropdown */}
<div className="dropdown mb-3 ms-2">
</div>
{/* Notes List */}
<div className="d-flex flex-column text-start" style={{ gap: "0rem", minHeight: "100%" }}>
{currentItems.map((noteItem) => (
<NoteCardDirectoryEditable
@ -132,53 +128,12 @@ const NotesCardViewDirectory = ({
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
);
}}
onNoteDelete={() => fetchNotes(projectId)} // reload with projectId
onNoteDelete={() => fetchNotes(projectId)}
/>
))}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="d-flex justify-content-end mt-2 align-items-center gap-2"
style={{ marginBottom: '70px' }}>
{/* Previous Button */}
<button
className="btn btn-sm rounded-circle border text-secondary"
onClick={() => handlePageClick(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
title="Previous"
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem" }} // Adjusted width, height, and font size
>
«
</button>
{/* Page Number Buttons */}
{[...Array(totalPages)].map((_, i) => {
const page = i + 1;
return (
<button
key={page}
className={`btn rounded-circle border ${page === currentPage ? "btn-primary text-white" : "btn-light text-secondary"}`}
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem", lineHeight: "1" }} // Adjusted width, height, and font size
onClick={() => handlePageClick(page)}
>
{page}
</button>
);
})}
{/* Next Button */}
<button
className="btn btn-sm rounded-circle border text-secondary"
onClick={() => handlePageClick(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
title="Next"
style={{ width: "30px", height: "30px", padding: 0, fontSize: "0.75rem" }} // Adjusted width, height, and font size
>
»
</button>
</div>
)}
</div>
);
};

View File

@ -1,45 +1,44 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import Editor from "../common/TextEditor/Editor";
import Avatar from "../common/Avatar";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import moment from "moment";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import NoteCardDirectory from "./NoteCardDirectory";
import showToast from "../../services/toastService";
import { useContactNotes } from "../../hooks/useDirectory";
import {
useActiveInActiveNote,
useContactNotes1,
useCreateNote,
useUpdateNote,
} from "../../hooks/useDirectory";
import { NoetCard } from "./DirectoryPageSkeleton";
const schema = z.object({
note: z.string().min(1, { message: "Note is required" }),
});
const NotesDirectory = ({
refetchProfile,
isLoading,
contactProfile, // This contactProfile now reliably includes firstName, middleName, lastName, and fullName
setProfileContact,
}) => {
const [IsActive, setIsActive] = useState(true);
const { contactNotes, refetch } = useContactNotes(
contactProfile?.id,
IsActive
);
const [IsSubmitting, setIsSubmitting] = useState(false);
const NotesDirectory = ({ contactId,contactPerson }) => {
const [isActive, setIsActive] = useState(true);
const [showEditor, setShowEditor] = useState(false);
// Queries & mutations
const { data, isError, isLoading } = useContactNotes1(contactId, isActive);
const { mutate: createNote, isPending } = useCreateNote(() =>
setShowEditor(false)
);
const { mutate: updateNote } = useUpdateNote();
const { mutate: toggleNoteStatus } = useActiveInActiveNote();
const {
register,
handleSubmit,
setValue,
watch,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
note: "",
},
defaultValues: { note: "" },
});
const noteValue = watch("note");
@ -48,116 +47,53 @@ const NotesDirectory = ({
setValue("note", value, { shouldValidate: true });
};
const onSubmit = async (data) => {
const newNote = { ...data, contactId: contactProfile?.id };
try {
setIsSubmitting(true);
const response = await DirectoryRepository.CreateNote(newNote);
const createdNote = response.data;
setProfileContact((prev) => ({
...prev,
notes: [...(prev.notes || []), createdNote],
}));
const cached_contactProfile = getCachedData("Contact Profile");
if (
cached_contactProfile &&
cached_contactProfile.contactId === contactProfile?.id
) {
const updatedProfile = {
...cached_contactProfile.data,
notes: [...(cached_contactProfile.data.notes || []), createdNote],
};
cacheData("Contact Profile", {
contactId: contactProfile?.id,
data: updatedProfile,
});
}
setValue("note", "");
setIsSubmitting(false);
showToast("Note added successfully!", "success");
setShowEditor(false);
setIsActive(true);
refetch(contactProfile?.id, true);
} catch (error) {
setIsSubmitting(false);
const msg =
error.response?.data?.message ||
error.message ||
"Error occurred during API calling";
showToast(msg, "error");
}
};
const onCancel = () => {
setValue("note", "");
setShowEditor(false);
const onSubmit = (formData) => {
const notPayload = { ...formData, contactId };
createNote(notPayload);
};
const handleSwitch = () => {
setIsActive((prevIsActive) => {
const newState = !prevIsActive;
refetch(contactProfile?.id, newState);
return newState;
});
setIsActive((prev) => !prev);
};
// Use the fullName from contactProfile, which now includes middle and last names if available
const contactName =
contactProfile?.fullName || contactProfile?.firstName || "Contact";
const noNotesMessage = `Be the first to share your insights! ${contactName} currently has no notes.`;
const notesToDisplay = IsActive
? contactProfile?.notes || []
: contactNotes || [];
const hasNotes = notesToDisplay.length > 0;
const handleCancel =()=>{
reset()
setShowEditor(false)
}
return (
<div className="text-start mt-10">
<div className="d-flex align-items-center justify-content-between">
<div className="row w-100 align-items-center">
{hasNotes && (
{data?.length > 0 && (
<div className="col col-2">
<p className="fw-semibold m-0 ms-3">Notes :</p>
</div>
)}
<div className="col d-flex justify-content-end gap-2 pe-0">
{" "}
<div className="d-flex align-items-center justify-content-between">
{/* Switch */}
<label
className="switch switch-primary"
style={{
fontSize: "15px",
}}
style={{ fontSize: "15px" }}
>
<input
type="checkbox"
className="switch-input"
onChange={handleSwitch}
checked={!IsActive} // invert binding
checked={!isActive} // invert binding
style={{ transform: "scale(0.8)" }}
/>
<span
className="switch-toggle-slider"
style={{
width: "30px", // narrower slider
height: "15px", // shorter slider
}}
style={{ width: "30px", height: "15px" }}
>
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span
className="switch-label"
style={{
fontSize: "14px", // smaller label text
marginLeft: "-14px"
}}
style={{ fontSize: "14px", marginLeft: "-14px" }}
>
Include Deleted Notes
</span>
@ -189,6 +125,7 @@ const NotesDirectory = ({
</div>
</div>
{/* Editor */}
{showEditor && (
<div className="card m-2 mb-5 position-relative">
<span
@ -202,9 +139,9 @@ const NotesDirectory = ({
<form onSubmit={handleSubmit(onSubmit)}>
<Editor
value={noteValue}
loading={IsSubmitting}
loading={isPending}
onChange={handleEditorChange}
onCancel={onCancel}
onCancel={handleCancel}
onSubmit={handleSubmit(onSubmit)}
/>
{errors.note && (
@ -216,29 +153,25 @@ const NotesDirectory = ({
<div className="justify-content-start px-1 mt-1">
{isLoading && (
<div className="text-center">
{" "}
<p>Loading...</p>{" "}
</div>
<NoetCard/>
)}
{!isLoading && notesToDisplay.length > 0
? notesToDisplay
.slice()
.reverse()
.map((noteItem) => (
{!isLoading && data?.length > 0
? data.map((noteItem) => (
<NoteCardDirectory
refetchProfile={refetchProfile}
refetchNotes={refetch}
refetchContact={refetch}
noteItem={noteItem}
contactId={contactProfile?.id}
setProfileContact={setProfileContact}
key={noteItem.id}
noteItem={noteItem}
contactId={contactId}
// updateNote={updateNote}
// toggleNoteStatus={toggleNoteStatus}
/>
))
: !isLoading &&
!showEditor && (
<div className="text-center mt-5">{noNotesMessage}</div>
<div className="text-center mt-5">
{`Be the first to share your insights! ${contactPerson}
currently has no notes.`}
</div>
)}
</div>
</div>

View File

@ -1,562 +0,0 @@
import React, { useEffect, useState } from "react";
import {
useForm,
useFieldArray,
FormProvider,
useFormContext,
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TagInput from "../common/TagInput";
import IconButton from "../common/IconButton";
import useMaster, {
useContactCategory,
useContactTags,
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import {
useBuckets,
useDesignation,
useOrganization,
} from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
import InputSuggestions from "../common/InputSuggestion";
import Label from "../common/Label";
const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const [categoryData, setCategoryData] = useState([]);
const [TagsData, setTagsData] = useState([]);
const { data, loading } = useMaster();
const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { contactTags, loading: Tagloading } = useContactTags();
const [IsSubmitting, setSubmitting] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const dispatch = useDispatch();
const { organizationList } = useOrganization();
const { designationList } = useDesignation();
const [showSuggestions, setShowSuggestions] = useState(false);
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
const methods = useForm({
resolver: zodResolver(ContactSchema),
defaultValues: {
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
},
});
const {
register,
handleSubmit,
control,
getValues,
trigger,
setValue,
watch,
reset,
formState: { errors },
} = methods;
const {
fields: emailFields,
append: appendEmail,
remove: removeEmail,
} = useFieldArray({ control, name: "contactEmails" });
const {
fields: phoneFields,
append: appendPhone,
remove: removePhone,
} = useFieldArray({ control, name: "contactPhones" });
const handleAddEmail = async () => {
const emails = getValues("contactEmails");
const lastIndex = emails.length - 1;
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
if (valid) {
appendEmail({ label: "Work", emailAddress: "" });
}
};
const handleAddPhone = async () => {
const phones = getValues("contactPhones");
const lastIndex = phones.length - 1;
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
if (valid) {
appendPhone({ label: "Office", phoneNumber: "" });
}
};
// handle logic when input of desgination is changed
const handleDesignationChange = (e) => {
const val = e.target.value;
const matches = designationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
setFilteredDesignationList(matches);
setShowSuggestions(true);
setTimeout(() => setShowSuggestions(false), 5000);
};
// handle logic when designation is selected
const handleSelectDesignation = (val) => {
setShowSuggestions(false);
setValue("designation", val);
};
const handlePhoneInput = (e) => {
const value = e.target.value.replace(/[^0-9]/g, "");
e.target.value = value.slice(0, 10);
};
const watchBucketIds = watch("bucketIds");
const toggleBucketId = (id) => {
const updated = watchBucketIds?.includes(id)
? watchBucketIds.filter((val) => val !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const handleCheckboxChange = (id) => {
const updated = watchBucketIds.includes(id)
? watchBucketIds.filter((i) => i !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const onSubmit = async (data) => {
const cleaned = {
...data,
contactEmails: (data.contactEmails || [])
.filter((e) => e.emailAddress?.trim() !== "")
.map((email, index) => {
const existingEmail = existingContact.contactEmails?.[index];
return existingEmail ? { ...email, id: existingEmail.id } : email;
}),
contactPhones: (data.contactPhones || [])
.filter((p) => p.phoneNumber?.trim() !== "")
.map((phone, index) => {
const existingPhone = existingContact.contactPhones?.[index];
return existingPhone ? { ...phone, id: existingPhone.id } : phone;
}),
};
setSubmitting(true);
await submitContact({ ...cleaned, id: existingContact.id });
setSubmitting(false);
};
const orgValue = watch("organization");
const handleClosed = () => {
onCLosed();
};
useEffect(() => {
const isValidContact =
existingContact &&
typeof existingContact === "object" &&
!Array.isArray(existingContact);
if (!isInitialized && isValidContact && TagsData) {
reset({
name: existingContact.name || "",
organization: existingContact.organization || "",
contactEmails: existingContact.contactEmails || [],
contactPhones: existingContact.contactPhones || [],
contactCategoryId: existingContact.contactCategory?.id || null,
address: existingContact.address || "",
description: existingContact.description || "",
designation: existingContact.designation || "",
projectIds: existingContact.projectIds || null,
tags: existingContact.tags || [],
bucketIds: existingContact.bucketIds || [],
});
if (
!existingContact.contactPhones ||
existingContact.contactPhones.length === 0
) {
appendPhone({ label: "Office", phoneNumber: "" });
}
if (
!existingContact.contactEmails ||
existingContact.contactEmails.length === 0
) {
appendEmail({ label: "Work", emailAddress: "" });
}
setIsInitialized(true);
}
// return()=> reset()
}, [existingContact, buckets, projects]);
return (
<FormProvider {...methods}>
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-center align-items-center">
<h5 className="m-0 fw-18"> Update Contact</h5>
</div>
<div className="row">
<div className="col-md-6 text-start">
<label className="form-label">Name</label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="col-md-6 text-start">
<label className="form-label">Organization</label>
<InputSuggestions
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
{errors.organization && (
<small className="danger-text">
{errors.organization.message}
</small>
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6 text-start">
<Label className="form-label" required>Designation</Label>
<input
className="form-control form-control-sm"
{...register("designation")}
onChange={handleDesignationChange}
/>
{showSuggestions && filteredDesignationList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-50 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius: "0px",
}}
>
{filteredDesignationList.map((designation) => (
<li
key={designation}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectDesignation(designation)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{designation}
</li>
))}
</ul>
)}
{errors.designation && (
<small className="danger-text">
{errors.designation.message}
</small>
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6">
{emailFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-1"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactEmails.${index}.label`)}
>
<option value="Work">Work</option>
<option value="Personal">Personal</option>
<option value="Other">Other</option>
</select>
{errors.contactEmails?.[index]?.label && (
<small className="danger-text">
{errors.contactEmails[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Email</label>
<div className="d-flex align-items-center">
<input
type="email"
className="form-control form-control-sm"
{...register(`contactEmails.${index}.emailAddress`)}
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddEmail}
/>
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1 p-0"
// onClick={() => removeEmail(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removeEmail(index)}
/>
)}
</div>
{errors.contactEmails?.[index]?.emailAddress && (
<small className="danger-text">
{errors.contactEmails[index].emailAddress.message}
</small>
)}
</div>
</div>
))}
</div>
<div className="col-md-6">
{phoneFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-2"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactPhones.${index}.label`)}
>
<option value="Office">Office</option>
<option value="Personal">Personal</option>
<option value="Business">Business</option>
</select>
{errors.phone?.[index]?.label && (
<small className="danger-text">
{errors.ContactPhones[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Phone</label>
<div className="d-flex align-items-center">
<input
type="tel"
className="form-control form-control-sm"
{...register(`contactPhones.${index}.phoneNumber`)}
placeholder="9876543210"
onInput={handlePhoneInput}
maxLength={10}
/>
{index === phoneFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// onClick={handleAddPhone}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddPhone}
/>
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1"
// onClick={() => removePhone(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removePhone(index)}
/>
)}
</div>
{errors.contactPhones?.[index]?.phoneNumber && (
<small className="danger-text">
{errors.contactPhones[index].phoneNumber.message}
</small>
)}
</div>
</div>
))}
</div>
{errors.contactPhone?.message && (
<div className="danger-text">{errors.contactPhone.message}</div>
)}
</div>
<div className="row my-1">
<div className="col-md-6 text-start">
<label className="form-label">Category</label>
<select
className="form-select form-select-sm"
{...register("contactCategoryId")}
>
{contactCategoryLoading && !contactCategory ? (
<option disabled value="">
Loading...
</option>
) : (
<>
<option disabled value="">
Select Category
</option>
{contactCategory?.map((cate) => (
<option key={cate.id} value={cate.id}>
{cate.name}
</option>
))}
</>
)}
</select>
{errors.contactCategoryId && (
<small className="danger-text">
{errors.contactCategoryId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<SelectMultiple
name="projectIds"
label="Select Projects"
options={projects}
labelKey="name"
valueKey="id"
IsLoading={projectLoading}
/>
{errors.projectIds && (
<small className="danger-text">{errors.projectIds.message}</small>
)}
</div>
</div>
<div className="col-12 text-start">
<TagInput name="tags" label="Tags" options={contactTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
<div className="row">
<div className="col-md-12 mt-1 text-start">
<label className="form-label ">Select Label</label>
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
{bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => (
<li
key={item.id}
className="list-inline-item flex-shrink-0 me-6 mb-2"
>
<div className="form-check ">
<input
type="checkbox"
className="form-check-input"
id={`item-${item.id}`}
checked={watchBucketIds.includes(item.id)}
onChange={() => handleCheckboxChange(item.id)}
/>
<label
className="form-check-label"
htmlFor={`item-${item.id}`}
>
{item.name}
</label>
</div>
</li>
))}
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
)}
</ul>
</div>
</div>
<div className="col-12 text-start">
<label className="form-label">Address</label>
<textarea
className="form-control form-control-sm"
rows="2"
{...register("address")}
/>
</div>
<div className="col-12 text-start">
<label className="form-label">Description</label>
<textarea
className="form-control form-control-sm"
rows="2"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">{errors.description.message}</small>
)}
</div>
<div className="d-flex justify-content-end gap-2 py-0 mt-4">
<button
className="btn btn-sm btn-label-secondary"
type="button"
onClick={handleClosed}
disabled={IsSubmitting}
>
Cancel
</button>
<button
className="btn btn-sm btn-primary"
type="submit"
disabled={IsSubmitting}
>
{IsSubmitting ? "Please Wait..." : "Update"}
</button>
</div>
</form>
</FormProvider>
);
};
export default UpdateContact;

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import moment from "moment";
import DateRangePicker from "../common/DateRangePicker";
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
import { useDispatch, useSelector } from "react-redux";
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
import usePagination from "../../hooks/usePagination";
@ -11,6 +11,12 @@ import AttendLogs from "../Activities/AttendLogs";
import { useAttendanceByEmployee } from "../../hooks/useAttendance";
import GlobalModel from "../common/GlobalModel";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { localToUtc } from "../../utils/appUtils";
const EmpAttendance = ({ employee }) => {
const [attendances, setAttendnaces] = useState([]);
@ -18,6 +24,21 @@ const EmpAttendance = ({ employee }) => {
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const [isModalOpen, setIsModalOpen] = useState(false);
const [attendanceId, setAttendanecId] = useState();
const methods = useForm({
resolver: zodResolver(z.object({
startDate: z.string(),
endDate: z.string()
})),
defaultValues: {
startDate: "",
endDate: ""
},
});
const { control, register, handleSubmit, reset, watch } = methods;
const startDate = watch('startDate')
const endDate = watch('endDate')
const {
data = [],
isLoading: loading,
@ -25,7 +46,7 @@ const EmpAttendance = ({ employee }) => {
isError,
error,
refetch,
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
} = useAttendanceByEmployee(employee, localToUtc(startDate), localToUtc(endDate));
const dispatch = useDispatch();
// const { data, loading, error } = useSelector(
@ -114,6 +135,11 @@ const EmpAttendance = ({ employee }) => {
};
const closeModal = () => setIsModalOpen(false);
const onSubmit = (formData) => {
}
return (
<>
{isModalOpen && (
@ -127,16 +153,25 @@ const EmpAttendance = ({ employee }) => {
id="DataTables_Table_0_length"
>
<div className="col-md-4 my-0 ">
<DateRangePicker
DateDifference="7"
onRangeChange={setDateRange}
endDateMode="today"
<>
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={true}
/>
</form>
</FormProvider>
</>
</div>
<div className="col-md-2 m-0 text-end">
<i
className={`bx bx-refresh cursor-pointer fs-4 ${
isFetching ? "spin" : ""
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
}`}
data-toggle="tooltip"
title="Refresh"
@ -230,8 +265,7 @@ const EmpAttendance = ({ employee }) => {
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${
currentPage === index + 1 ? "active" : ""
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
@ -243,8 +277,7 @@ const EmpAttendance = ({ employee }) => {
</li>
))}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button

View File

@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
import { formatFileSize } from "../../utils/appUtils";
import { formatFileSize, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
@ -183,9 +183,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const onSubmit = (fromdata) => {
let payload = {
...fromdata,
transactionDate: moment
.utc(fromdata.transactionDate, "DD-MM-YYYY")
.toISOString(),
transactionDate: localToUtc(fromdata.transactionDate)
};
if (expenseToEdit) {
const editPayload = { ...payload, id: data.id };
@ -331,7 +330,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<Label htmlFor="transactionDate" className="form-label" required>
Transaction Date
</Label>
<DatePicker name="transactionDate" control={control} />
<DatePicker name="transactionDate" control={control} maxDate={new Date()}/>
{errors.transactionDate && (
<small className="danger-text">

View File

@ -9,7 +9,7 @@ import ProjectRepository, {
TasksRepository,
} from "../../repositories/ProjectRepository";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT_INFRA } from "../../utils/constants";
import { MANAGE_PROJECT_INFRA, MANAGE_TASK } from "../../utils/constants";
import InfraTable from "./Infrastructure/InfraTable";
import {
cacheData,
@ -34,6 +34,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
const [ project, setProject ] = useState( projects_Details );
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageTask = useHasUserPermission(MANAGE_TASK)
const [showModalFloor, setshowModalFloor] = useState(false);
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
const [showModalTask, setshowModalTask] = useState(false);
@ -87,13 +88,12 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<div className="align-items-center">
<div className="row ">
<div
className={`col-12 text-end mb-1 ${
!ManageInfra && "d-none"
} `}
className={`col-12 text-end mb-1 `}
>
{ManageInfra && (<>
<button
type="button"
className="link-button link-button-sm m-1 btn-primary"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={()=>setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
@ -101,7 +101,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
</button>
<button
type="button"
className="link-button m-1 btn-primary"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
@ -109,20 +109,21 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
</button>
<button
type="button"
className="link-button m-1 btn-primary"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
</button>
<button
</button></>)}
{(ManageTask || ManageInfra) && (<button
type="button"
className="link-button m-1 btn-primary"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalTask(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Create Tasks
</button>
</button>)}
</div>
</div>
<div className="row ">

View File

@ -5,11 +5,15 @@ import {
DIRECTORY_ADMIN,
DIRECTORY_MANAGER,
DIRECTORY_USER,
MANAGE_PROJECT_INFRA,
MANAGE_TASK,
VIEW_PROJECT_INFRA,
} from "../../utils/constants";
const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
const HasManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const HasManageTask = useHasUserPermission(MANAGE_TASK)
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirUser = useHasUserPermission(DIRECTORY_USER);
@ -21,7 +25,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
key: "infra",
icon: "bx bx-grid-alt",
label: "Infrastructure",
hidden: !HasViewInfraStructure,
hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
},
{
key: "directory",

View File

@ -44,18 +44,19 @@ const ProjectPermission = () => {
);
useEffect(() => {
if (!employees.length) return;
if (!selectedEmployee) return;
const enabledPerms =
selectedEmpPermissions?.permissions
?.filter((perm) => perm.isEnabled)
?.map((perm) => perm.id) || [];
reset({
employeeId: selectedEmployee || employees[0]?.id || "",
reset((prev) => ({
...prev,
selectedPermissions: enabledPerms,
});
}, [selectedEmpPermissions, reset, selectedEmployee, employees]);
}));
}, [selectedEmpPermissions, reset, selectedEmployee]);
const { mutate: updatePermission, isPending } =
useUpdateProjectLevelEmployeePermission();
@ -67,30 +68,23 @@ const ProjectPermission = () => {
}
const existingPermissions = selectedEmpPermissions?.permissions || [];
const existingEnabledIds = existingPermissions
.filter((p) => p.isEnabled)
.map((p) => p.id);
const payloadPermissions =
existingPermissions.length > 0
? existingPermissions.map((perm) => ({
id: perm.id,
isEnabled: formData.selectedPermissions?.includes(perm.id) || false,
}))
: (formData.selectedPermissions || []).map((id) => ({
id,
isEnabled: true,
}));
const newSelectedIds = formData.selectedPermissions || [];
const removed = existingEnabledIds
.filter((id) => !newSelectedIds.includes(id))
.map((id) => ({ id, isEnabled: false }));
const added = newSelectedIds
.filter((id) => !existingEnabledIds.includes(id))
.map((id) => ({ id, isEnabled: true }));
const payloadPermissions = [...removed, ...added];
if (payloadPermissions.length === 0) {
showToast("No permissions selected", "warn");
return;
}
const hasChanges = existingPermissions.some(
(perm) =>
perm.isEnabled !==
(formData.selectedPermissions?.includes(perm.id) || false)
);
if (!hasChanges && existingPermissions.length > 0) {
showToast("No changes detected", "info");
return;
}
@ -104,6 +98,7 @@ const ProjectPermission = () => {
updatePermission(payload);
};
return (
<div className="row px-2 py-1">
<form className="row" onSubmit={handleSubmit(onSubmit)}>
@ -120,8 +115,13 @@ const ProjectPermission = () => {
<option value="">Loading...</option>
) : (
<>
<option value="">-- Select --</option>
{employees.map((emp) => (
<option value="">-- Select Employee --</option>
{[...employees]?.sort((a, b) =>
`${a.firstName} ${a.firstName}`?.localeCompare(
`${b.firstName} ${b.lastName}`
)
)
?.map((emp) => (
<option key={emp.id} value={emp.id}>
{emp.firstName} {emp.lastName}
</option>
@ -129,13 +129,17 @@ const ProjectPermission = () => {
</>
)}
</select>
{errors.employeeId && (
<div className="text-danger small">
{errors.employeeId.message}
</div>
)}
</div>
<button className="btn btn-sm btn-primary" disabled={isPending || loading}>
<button
className="btn btn-sm btn-primary"
disabled={isPending || loading}
>
{isPending ? "Please Wait..." : "Update Permission"}
</button>
</div>
@ -149,10 +153,7 @@ const ProjectPermission = () => {
<div className="col-12">
<div className="row">
{feature.featurePermissions?.map((perm) => (
<div
className="col-12 col-sm-6 col-md-4 mb-2"
key={perm.id}
>
<div className="col-12 col-sm-6 col-md-4 mb-2" key={perm.id}>
<label
className="form-check-label d-flex align-items-center"
htmlFor={perm.id}

View File

@ -32,8 +32,8 @@ const ProjectSetting = () => {
return (
<div className="w-100">
<div className="card py-2 px-5">
<div className="col-4">
<div className="dropdown text-start">
<div className="col-12">
<div className="dropdown text-end">
<button
className="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"

View File

@ -8,7 +8,6 @@ const InputSuggestions = ({
}) => {
const [filteredList, setFilteredList] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const handleInputChange = (e) => {
const val = e.target.value;
onChange(val);

View File

@ -74,7 +74,7 @@ const Editor = ({
{/* Right: Submit + Cancel Buttons */}
<div className="d-flex justify-content-end gap-2 p-1">
<span
className="btn btn-xs btn-secondary"
className="btn btn-xs btn-label-secondary"
aria-disabled={loading}
onClick={onCancel}
>

View File

@ -108,7 +108,7 @@ export const useAttendanceByEmployee = (employeeId, fromDate, toDate) => {
const res = await AttendanceRepository.getAttendanceByEmployee(employeeId, fromDate, toDate);
return res.data;
},
enabled,
enabled: !!fromDate && !! toDate,
});
};

View File

@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { DirectoryRepository } from "../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../slices/apiDataManager";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import showToast from "../services/toastService";
import { queryClient } from "../layouts/AuthLayout";
export const useDirectory = (isActive, prefernceContacts) => {
const [contacts, setContacts] = useState([]);
@ -10,7 +13,10 @@ export const useDirectory = (isActive,prefernceContacts) => {
const fetch = async (activeParam = isActive) => {
setLoading(true);
try {
const response = await DirectoryRepository.GetContacts(activeParam,prefernceContacts);
const response = await DirectoryRepository.GetContacts(
activeParam,
prefernceContacts
);
setContacts(response.data);
cacheData("contacts", { data: response.data, isActive: activeParam });
} catch (error) {
@ -22,7 +28,11 @@ export const useDirectory = (isActive,prefernceContacts) => {
useEffect(() => {
const cachedContacts = getCachedData("contacts");
if (!cachedContacts?.data || cachedContacts.isActive !== isActive || prefernceContacts) {
if (
!cachedContacts?.data ||
cachedContacts.isActive !== isActive ||
prefernceContacts
) {
fetch(isActive, prefernceContacts);
} else {
setContacts(cachedContacts.data);
@ -77,7 +87,6 @@ export const useContactProfile = (id) => {
const [Error, setError] = useState("");
const fetchContactProfile = async () => {
setLoading(true);
try {
const resp = await DirectoryRepository.GetContactProfile(id);
@ -85,18 +94,14 @@ export const useContactProfile = (id) => {
cacheData("Contact Profile", { data: resp.data, contactId: id });
} catch (err) {
const msg =
err?.response?.data?.message ||
err?.message ||
"Something went wrong";
err?.response?.data?.message || err?.message || "Something went wrong";
setError(msg);
} finally {
setLoading(false);
}
};
useEffect( () =>
{
useEffect(() => {
const cached = getCachedData("Contact Profile");
if (!cached || cached.contactId !== id) {
fetchContactProfile(id);
@ -114,8 +119,6 @@ export const useContactNotes = (id, IsActive) => {
const [Error, setError] = useState("");
const fetchContactNotes = async (id, IsActive) => {
setLoading(true);
try {
const resp = await DirectoryRepository.GetNote(id, IsActive);
@ -123,14 +126,11 @@ export const useContactNotes = (id, IsActive) => {
cacheData("Contact Notes", { data: resp.data, contactId: id });
} catch (err) {
const msg =
err?.response?.data?.message ||
err?.message ||
"Something went wrong";
err?.response?.data?.message || err?.message || "Something went wrong";
setError(msg);
} finally {
setLoading(false);
}
};
useEffect(() => {
@ -210,3 +210,378 @@ export const useDesignation = () => {
return { designationList, loading, error };
};
// ------------------------------Query------------------------------------------------------------------
export const useBucketList = () => {
return useQuery({
queryKey: ["bucketList"],
queryFn: async () => {
const resp = await DirectoryRepository.GetBucktes();
return resp.data;
},
});
};
export const useDirectoryNotes = (
pageSize,
pageNumber,
filter,
searchString
) => {
return useQuery({
queryKey: ["directoryNotes", pageSize, pageNumber, filter, searchString],
queryFn: async () =>
await DirectoryRepository.GetBucktes(
pageSize,
pageNumber,
filter,
searchString
),
});
};
const cleanFilter = (filter) => {
const cleaned = { ...filter };
["bucketIds", "categoryIds"].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
});
return cleaned;
};
export const useContactList = (
isActive,
projectId,
pageSize,
pageNumber,
filter,
searchString = ""
) => {
return useQuery({
queryKey: [
"contacts",
isActive,
projectId,
pageSize,
pageNumber,
filter,
searchString,
],
queryFn: async () => {
const cleanedFilter = cleanFilter(filter);
const resp = await DirectoryRepository.GetContacts(
isActive,
projectId,
pageSize,
pageNumber,
cleanedFilter,
searchString
);
return resp.data;
},
});
};
export const useContactFilter = () => {
return useQuery({
queryKey: ["contactFilter"],
queryFn: async () => {
const resp = await DirectoryRepository.GetContactFilter();
return resp.data;
},
});
};
export const useContactDetails = (contactId) => {
return useQuery({
queryKey: ["Contact", contactId],
queryFn: async () => {
const resp = await await DirectoryRepository.GetContact(contactId);
return resp.data;
},
enabled: !!contactId,
});
};
const cleanNoteFilter = (filter) => {
const cleaned = { ...filter };
["createdByIds", "organizations"].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
});
return cleaned;
};
export const useNotes = (
projectId,
pageSize,
pageNumber,
filter,
searchString = ""
) => {
return useQuery({
queryKey: ["Notes", projectId, pageSize, pageNumber, filter, searchString],
queryFn: async () => {
const cleanedFilter = cleanNoteFilter(filter);
const resp = await DirectoryRepository.GetNotes(
projectId,
pageSize,
pageNumber,
cleanedFilter,
searchString
);
return resp.data;
},
});
};
export const useNoteFilter = () => {
return useQuery({
queryKey: ["NoteFilter"],
queryFn: async () => {
const resp = await DirectoryRepository.GetNoteFilter();
return resp.data;
},
});
};
export const useContactProfile1 = (contactId) => {
return useQuery({
queryKey: ["ContactProfile", contactId],
queryFn: async () => {
const resp = await DirectoryRepository.GetContactProfile(contactId);
return resp.data;
},
enabled: !!contactId,
});
};
export const useContactNotes1 = (contactId, active) => {
return useQuery({
queryKey: ["ContactNotes", contactId, active],
queryFn: async () => {
const resp = await DirectoryRepository.GetContactNotes(contactId, active);
return resp.data;
},
enabled: !!contactId,
});
};
// ---------------------------Mutation------------------------------------------------------------------
export const useCreateBucket = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (BucketPayload) =>
await DirectoryRepository.CreateBuckets(BucketPayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
showToast("Bucket created Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useUpdateBucket = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ bucketId, BucketPayload }) =>
await DirectoryRepository.UpdateBuckets(bucketId, BucketPayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
showToast("Bucket updated successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
"Something went wrong. Please try again later.",
"error"
);
},
});
};
export const useAssignEmpToBucket = () => {
return useMutation({
mutationFn: async ({ bucketId, EmployeePayload }) =>
await DirectoryRepository.AssignedBuckets(bucketId, EmployeePayload),
onSuccess: (_, variables) => {
const { EmployeePayload } = variables;
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
showToast(
`Bucket shared ${EmployeePayload?.length} Employee Successfully`,
"success"
);
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useDeleteBucket = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (bucketId) =>
await DirectoryRepository.DeleteBucket(bucketId),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["bucketList"] });
showToast("Bucket deleted Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useCreateContact = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (contactPayload) =>
await DirectoryRepository.CreateContact(contactPayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["contacts"] });
showToast("Contact created Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useUpdateContact = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ contactId, contactPayload }) =>
await DirectoryRepository.UpdateContact(contactId, contactPayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["contacts"] });
showToast("Contact updated Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useActiveInActiveContact = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ contactId, contactStatus }) =>
await DirectoryRepository.DeleteContact(contactId, contactStatus),
onSuccess: (_, variables) => {
const { contactStatus } = variables;
queryClient.invalidateQueries({ queryKey: ["contacts"] });
showToast(
`Contact ${contactStatus ? "Restored" : "Deleted"} Successfully`,
"success"
);
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useCreateNote = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (notPayload) =>
await DirectoryRepository.CreateNote(notPayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["Notes"] });
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
showToast(`Note Created Successfully`, "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useUpdateNote = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ noteId, notePayload }) =>
await DirectoryRepository.UpdateNote(noteId, notePayload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["Notes"] });
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
showToast("Note updated Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};
export const useActiveInActiveNote = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ noteId, noteStatus }) =>
await DirectoryRepository.DeleteNote(noteId, noteStatus),
onSuccess: (_, variables) => {
const { noteStatus } = variables;
queryClient.invalidateQueries({ queryKey: ["Notes"] });
queryClient.invalidateQueries({ queryKey: ["ContactNotes"] });
showToast(
`Note ${noteStatus ? "Restored" : "Deleted"} Successfully`,
"success"
);
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.response.data.message ||
"Something went wrong.Please try again later.",
"error"
);
},
});
};

View File

@ -0,0 +1,77 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
contactsFilter,
defaultContactFilter,
} from "../../components/Directory/DirectorySchema";
import { useContactFilter } from "../../hooks/useDirectory";
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
import SelectMultiple from "../../components/common/SelectMultiple";
const ContactFilterPanel = ({ onApply, clearFilter }) => {
const { data, isError, isLoading, error, isFetched, isFetching } =
useContactFilter();
const methods = useForm({
resolver: zodResolver(contactsFilter),
defaultValues: defaultContactFilter,
});
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const { register, handleSubmit, reset, watch } = methods;
const onSubmit = (formData) => {
onApply(formData);
closePanel();
};
const handleClose = () => {
reset(defaultContactFilter);
onApply(defaultContactFilter);
closePanel();
};
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="row g-2">
<SelectMultiple
name="bucketIds"
label="Buckets :"
options={data.buckets}
labelKey="name"
valueKey="id"
/>
<SelectMultiple
name="categoryIds"
label="Contact Category :"
options={data.contactCategories}
labelKey={(item) => item.name}
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
onClick={handleClose}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default ContactFilterPanel;

View File

@ -0,0 +1,112 @@
import React, { useEffect, useState } from "react";
import { useFab } from "../../Context/FabContext";
import { useContactList } from "../../hooks/useDirectory";
import { useDirectoryContext } from "./DirectoryPage";
import CardViewContact from "../../components/Directory/CardViewContact";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import ContactFilterPanel from "./ContactFilterPanel";
import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
import { useDebounce } from "../../utils/appUtils";
import Pagination from "../../components/common/Pagination";
import ListViewContact from "../../components/Directory/ListViewContact";
import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
// Utility function to format contacts for CSV export
const formatExportData = (contacts) => {
return contacts.map(contact => ({
Email: contact.contactEmails?.map(e => e.emailAddress).join(", ") || "",
Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "",
Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "",
Location: contact.address || "",
Organization: contact.organization || "",
Category: contact.contactCategory?.name || "",
Tags: contact.tags?.map(t => t.name).join(", ") || "",
Buckets: contact.bucketIds?.join(", ") || "",
}));
};
const ContactsPage = ({ projectId, searchText, onExport }) => {
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilter] = useState(defaultContactFilter);
const debouncedSearch = useDebounce(searchText, 500);
const { showActive, gridView } = useDirectoryContext();
const { data, isError, isLoading, error } = useContactList(
showActive,
projectId,
ITEMS_PER_PAGE,
currentPage,
filters,
debouncedSearch
);
const { setOffcanvasContent, setShowTrigger } = useFab();
const clearFilter = () => setFilter(defaultContactFilter);
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent(
"Contacts Filters",
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
// 🔹 Format contacts for export
useEffect(() => {
if (data?.data && onExport) {
onExport(formatExportData(data.data));
}
}, [data?.data]);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isError) return <div>{error.message}</div>;
if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return (
<div className="row mt-5">
{gridView ? (
<>
{data?.data?.map((contact) => (
<div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
<CardViewContact IsActive={showActive} contact={contact} />
</div>
))}
{data?.data?.length > 0 && (
<div className="col-12 d-flex justify-content-start mt-3">
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
</div>
)}
</>
) : (
<div className="col-12">
<ListViewContact
data={data.data}
Pagination={
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
}
/>
</div>
)}
</div>
);
};
export default ContactsPage;

Some files were not shown because too many files have changed in this diff Show More