Merge pull request 'Refactor_Directory And Project Level Permsssion' (#404) from Refactor_Directory into main
Reviewed-on: #404 Merged
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 308 KiB |
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 411 KiB After Width: | Height: | Size: 411 KiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 787 KiB After Width: | Height: | Size: 787 KiB |
Before Width: | Height: | Size: 786 KiB After Width: | Height: | Size: 786 KiB |
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 295 KiB |
Before Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 401 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
1
public/img/icons/inventory.svg
Normal 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 |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 860 B After Width: | Height: | Size: 860 B |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 860 KiB After Width: | Height: | Size: 860 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@ -99,9 +99,7 @@ const AttendanceOverview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-white p-4 rounded shadow d-flex flex-column">
|
||||||
className="bg-white p-4 rounded shadow d-flex flex-column"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div className="card-title mb-0 text-start">
|
<div className="card-title mb-0 text-start">
|
||||||
@ -119,18 +117,22 @@ const AttendanceOverview = () => {
|
|||||||
<option value={30}>Last 30 Days</option>
|
<option value={30}>Last 30 Days</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${
|
||||||
|
view === "chart" ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setView("chart")}
|
onClick={() => setView("chart")}
|
||||||
title="Chart View"
|
title="Chart View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-bar-chart-alt-2"></i>
|
<i className="bx bx-bar-chart-alt-2"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${view === "table" ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${
|
||||||
|
view === "table" ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setView("table")}
|
onClick={() => setView("table")}
|
||||||
title="Table View"
|
title="Table View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-task text-success"></i>
|
<i class="bx bx-list-ul fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
68
src/components/Directory/AssignedBucket.jsx
Normal 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;
|
95
src/components/Directory/BucketForm.jsx
Normal 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;
|
53
src/components/Directory/BucketList.jsx
Normal 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;
|
217
src/components/Directory/CardViewContact.jsx
Normal 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;
|
@ -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;
|
|
@ -1,18 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useContactProfile } from "../../hooks/useDirectory";
|
import { useContactNotes, useContactProfile1 } from "../../hooks/useDirectory";
|
||||||
|
import { ContactProfileSkeleton } from "./DirectoryPageSkeleton";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
import moment from "moment";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import NotesDirectory from "./NotesDirectory";
|
import NotesDirectory from "./NotesDirectory";
|
||||||
|
|
||||||
const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
const ContactProfile = ({ contactId }) => {
|
||||||
const { contactProfile, loading, refetch } = useContactProfile(contact?.id);
|
const { data, isError, isLoading, error } = useContactProfile1(contactId.id);
|
||||||
const [copiedIndex, setCopiedIndex] = useState(null);
|
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||||
|
|
||||||
const [profileContactState, setProfileContactState] = useState(null);
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
// Safely access description, defaulting to an empty string if not present
|
const description = data?.description || "";
|
||||||
const description = profileContactState?.description || "";
|
|
||||||
const limit = 500;
|
const limit = 500;
|
||||||
|
|
||||||
const toggleReadMore = () => setExpanded(!expanded);
|
const toggleReadMore = () => setExpanded(!expanded);
|
||||||
@ -21,78 +19,32 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
const displayText = expanded
|
const displayText = expanded
|
||||||
? description
|
? description
|
||||||
: description.slice(0, limit) + (isLong ? "..." : "");
|
: description.slice(0, limit) + (isLong ? "..." : "");
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
useEffect(() => {
|
if (isLoading) return <ContactProfileSkeleton />;
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div className="text-center m-0 p-0">
|
<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>
|
<div>
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
classAvatar="m-0"
|
classAvatar="m-0"
|
||||||
firstName={
|
firstName={(data?.name || "").trim().split(" ")[0]?.charAt(0) || ""}
|
||||||
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
|
lastName={(data?.name || "").trim().split(" ")[1]?.charAt(0) || ""}
|
||||||
}
|
|
||||||
lastName={
|
|
||||||
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-column text-start ms-1">
|
<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">
|
<small className="text-secondary small-text">
|
||||||
{profileContactState?.designation}
|
{data?.designation}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row ms-9">
|
<div className="row ms-9">
|
||||||
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
<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 mb-2 align-items-start">
|
||||||
<div
|
<div
|
||||||
className="d-flex align-items-start"
|
className="d-flex align-items-start"
|
||||||
@ -107,14 +59,16 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<ul className="list-unstyled mb-0">
|
<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}>
|
<li className="d-flex align-items-center mb-1" key={idx}>
|
||||||
<span className="me-1 text-break overflow-wrap">
|
<span className="me-1 text-break overflow-wrap">
|
||||||
{email.emailAddress}
|
{email.emailAddress}
|
||||||
</span>
|
</span>
|
||||||
<i
|
<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"}
|
title={copiedIndex === idx ? "Copied!" : "Copy Email"}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
onClick={() => handleCopy(email.emailAddress, idx)}
|
onClick={() => handleCopy(email.emailAddress, idx)}
|
||||||
@ -126,7 +80,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.contactPhones?.length > 0 && (
|
{data?.contactPhones?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -138,10 +92,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<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}>
|
<li className="list-inline-item me-1" key={idx}>
|
||||||
{phone.phoneNumber}
|
{phone.phoneNumber}
|
||||||
{idx < profileContactState.contactPhones.length - 1 && ","}
|
{idx < data.contactPhones.length - 1 && ","}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -149,7 +103,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.createdAt && (
|
{data?.createdAt && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -160,14 +114,12 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span>
|
<span>{formatUTCToLocalTime(data.createdAt)}</span>
|
||||||
{moment(profileContactState.createdAt).format("DD MMMM, YYYY")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.address && (
|
{data?.address && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-start">
|
<span className="d-flex align-items-start">
|
||||||
@ -177,14 +129,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
<span style={{ marginLeft: "26px" }}>:</span>
|
<span style={{ marginLeft: "26px" }}>:</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-break small">{profileContactState.address}</span>
|
<span className="text-break small">{data.address}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6 d-flex flex-column text-start">
|
<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 mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -196,13 +148,13 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span style={{ wordBreak: "break-word" }}>
|
<span style={{ wordBreak: "break-word" }}>
|
||||||
{profileContactState.organization}
|
{data.organization}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.contactCategory && (
|
{data?.contactCategory && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -215,14 +167,14 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{profileContactState.contactCategory.name}
|
{data.contactCategory.name}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.tags?.length > 0 && (
|
{data?.tags?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -234,7 +186,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{profileContactState.tags.map((tag, index) => (
|
{data.tags.map((tag, index) => (
|
||||||
<li key={index} className="list-inline-item">
|
<li key={index} className="list-inline-item">
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</li>
|
</li>
|
||||||
@ -244,7 +196,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profileContactState?.buckets?.length > 0 && (
|
{data?.buckets?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -256,7 +208,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{profileContactState.buckets.map((bucket) => (
|
{data.buckets.map((bucket) => (
|
||||||
<li className="list-inline-item me-2" key={bucket.id}>
|
<li className="list-inline-item me-2" key={bucket.id}>
|
||||||
<span className="badge bg-label-primary my-1">
|
<span className="badge bg-label-primary my-1">
|
||||||
{bucket.name}
|
{bucket.name}
|
||||||
@ -269,7 +221,7 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profileContactState?.projects?.length > 0 && (
|
{data?.projects?.length > 0 && (
|
||||||
<div className="d-flex mb-2 align-items-start">
|
<div className="d-flex mb-2 align-items-start">
|
||||||
<div className="d-flex" style={{ minWidth: "130px" }}>
|
<div className="d-flex" style={{ minWidth: "130px" }}>
|
||||||
<span className="d-flex align-items-center">
|
<span className="d-flex align-items-center">
|
||||||
@ -280,11 +232,11 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline text-wrap mb-0">
|
||||||
{profileContactState.projects.map((project, index) => (
|
{data.projects.map((project, index) => (
|
||||||
<li className="list-inline-item me-2" key={project.id}>
|
<li className="list-inline-item me-2" key={project.id}>
|
||||||
{project.name}
|
{project.name}
|
||||||
{index < profileContactState.projects.length - 1 && ","}
|
{index < data.projects.length - 1 && ","}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -324,15 +276,10 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className="my-1" />
|
<hr className="my-1" />
|
||||||
<NotesDirectory
|
<NotesDirectory contactId={data?.id} contactPerson={data?.name} />
|
||||||
refetchProfile={refetch}
|
|
||||||
isLoading={loading}
|
|
||||||
contactProfile={profileContactState}
|
|
||||||
setProfileContact={setProfileContactState}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfileContactDirectory;
|
export default ContactProfile;
|
275
src/components/Directory/DirectoryPageSkeleton.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,47 +1,49 @@
|
|||||||
import { z } from "zod";
|
import { array, z } from "zod";
|
||||||
export const ContactSchema = z
|
export const ContactSchema = z.object({
|
||||||
.object({
|
name: z.string().min(1, "Name is required"),
|
||||||
name: z.string().min(1, "Name is required"),
|
organization: z.string().min(1, "Organization name is required"),
|
||||||
organization: z.string().min(1, "Organization name is required"),
|
contactCategoryId: z.string().nullable().optional(),
|
||||||
contactCategoryId: z.string().nullable().optional(),
|
address: z.string().optional(),
|
||||||
address: z.string().optional(),
|
description: z.string().min(1, { message: "Description is required" }),
|
||||||
description: z.string().min(1, { message: "Description is required" }),
|
designation: z.string().min(1, { message: "Designation is requried" }),
|
||||||
designation: z.string().min(1, {message:"Designation is requried"}),
|
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
|
||||||
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
|
contactEmails: z
|
||||||
contactEmails: z
|
.array(
|
||||||
.array(
|
z.object({
|
||||||
z.object({
|
label: z.string(),
|
||||||
label: z.string(),
|
emailAddress: z.string().email("Invalid email").or(z.literal("")),
|
||||||
emailAddress: z.string().email("Invalid email").or(z.literal("")),
|
})
|
||||||
})
|
)
|
||||||
)
|
.optional()
|
||||||
.optional()
|
.default([]),
|
||||||
.default([]),
|
|
||||||
|
|
||||||
contactPhones: z
|
contactPhones: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
phoneNumber: z
|
phoneNumber: z
|
||||||
.string()
|
.string()
|
||||||
.min(6, "Invalid Number")
|
.min(6, "Invalid Number")
|
||||||
.max(13, "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()
|
)
|
||||||
.default([]),
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
|
||||||
tags: z
|
tags: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string().nullable(),
|
id: z.string().nullable().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1, { message: "At least one tag is required" }),
|
.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) => {
|
// .refine((data) => {
|
||||||
// const hasValidEmail = (data.contactEmails || []).some(
|
// const hasValidEmail = (data.contactEmails || []).some(
|
||||||
@ -57,10 +59,43 @@ bucketIds: z.array(z.string()).nonempty({ message: "At least one bucket is requi
|
|||||||
// path: ["contactPhone"],
|
// path: ["contactPhone"],
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
// Buckets
|
// Buckets
|
||||||
|
|
||||||
export const bucketScheam = z.object( {
|
export const bucketScheam = z.object({
|
||||||
name: z.string().min( 1, {message: "Name is required"} ),
|
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: [],
|
||||||
|
};
|
||||||
|
@ -21,6 +21,6 @@ export const getPhoneIcon = (type) => {
|
|||||||
|
|
||||||
|
|
||||||
export const getBucketNameById = (buckets, id) => {
|
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';
|
return bucket ? bucket.name : 'Unknown';
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useSortableData } from "../../hooks/useSortableData";
|
import { useSortableData } from "../../hooks/useSortableData";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
|
|
||||||
const EmployeeList = ({ employees, onChange, bucket }) => {
|
const EmployeeList = ({ employees, onChange, bucket }) => {
|
||||||
const [employeefiltered, setEmployeeFilter] = useState([]);
|
const [employeefiltered, setEmployeeFilter] = useState([]);
|
||||||
const [employeeStatusList, setEmployeeStatusList] = useState([]);
|
const [employeeStatusList, setEmployeeStatusList] = useState([]);
|
||||||
@ -69,6 +70,8 @@ const EmployeeList = ({ employees, onChange, bucket }) => {
|
|||||||
`${employee?.firstName} ${employee?.lastName}`?.toLowerCase();
|
`${employee?.firstName} ${employee?.lastName}`?.toLowerCase();
|
||||||
return fullName.includes(searchTerm.toLowerCase());
|
return fullName.includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||||
|
203
src/components/Directory/ListViewContact.jsx
Normal 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 mx-5 pb-2">
|
||||||
|
<table className="table dataTable text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: "2px solid var(--bs-table-border-color)"}}>
|
||||||
|
{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;
|
@ -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;
|
|
@ -1,411 +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";
|
|
||||||
|
|
||||||
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-6 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`}
|
|
||||||
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">
|
|
||||||
<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">
|
|
||||||
<label htmlFor="bucketName" className="form-label">
|
|
||||||
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">
|
|
||||||
<label htmlFor="bucketDescription" className="form-label">
|
|
||||||
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-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBack}
|
|
||||||
className="btn btn-sm btn-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;
|
|
101
src/components/Directory/ManageBucket1.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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";
|
||||||
|
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
|
||||||
|
|
||||||
|
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 { setContactOpen, setDeleteBucket } = useDirectoryContext();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAction(null);
|
||||||
|
setSelectedBucket(null);
|
||||||
|
setDeleteId(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 == "create" ? (
|
||||||
|
<>
|
||||||
|
<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}
|
||||||
|
onDelete={(id) => setDeleteBucket({isOpen:true,bucketId:id})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ManageBucket1;
|
@ -1,65 +1,49 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import { useForm, useFieldArray, FormProvider } from "react-hook-form";
|
||||||
useForm,
|
|
||||||
useFieldArray,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import TagInput from "../common/TagInput";
|
import TagInput from "../common/TagInput";
|
||||||
import IconButton from "../common/IconButton";
|
import {
|
||||||
import useMaster, {
|
useBuckets,
|
||||||
|
useContactDetails,
|
||||||
|
useCreateContact,
|
||||||
|
useDesignation,
|
||||||
|
useOrganization,
|
||||||
|
useUpdateContact,
|
||||||
|
} from "../../hooks/useDirectory";
|
||||||
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
|
import {
|
||||||
useContactCategory,
|
useContactCategory,
|
||||||
useContactTags,
|
useContactTags,
|
||||||
} from "../../hooks/masterHook/useMaster";
|
} 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 SelectMultiple from "../common/SelectMultiple";
|
||||||
import { ContactSchema } from "./DirectorySchema";
|
import { ContactSchema, defaultContactValue } from "./DirectorySchema";
|
||||||
import InputSuggestions from "../common/InputSuggestion";
|
import InputSuggestions from "../common/InputSuggestion";
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
|
|
||||||
const ManageDirectory = ({ submitContact, onCLosed }) => {
|
const ManageContact = ({ contactId, closeModal }) => {
|
||||||
const selectedMaster = useSelector(
|
// fetch master data
|
||||||
(store) => store.localVariables.selectedMaster
|
|
||||||
);
|
|
||||||
const [categoryData, setCategoryData] = useState([]);
|
|
||||||
|
|
||||||
const [TagsData, setTagsData] = useState([]);
|
|
||||||
const { data, loading } = useMaster();
|
|
||||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
const { buckets, loading: bucketsLoaging } = useBuckets();
|
||||||
const { projects, loading: projectLoading } = useProjects();
|
const { projects, loading: projectLoading } = useProjects();
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
const { contactCategory, loading: contactCategoryLoading } =
|
||||||
useContactCategory();
|
useContactCategory();
|
||||||
const { organizationList, loading: orgLoading } = useOrganization();
|
const { organizationList } = useOrganization();
|
||||||
const { designationList, loading: designloading } = useDesignation();
|
const { designationList } = useDesignation();
|
||||||
const { contactTags, loading: Tagloading } = useContactTags();
|
const { contactTags } = useContactTags();
|
||||||
const [IsSubmitting, setSubmitting] = useState(false);
|
|
||||||
|
// fetch contact details if editing
|
||||||
|
const { data: contactData, isLoading: isContactLoading } = useContactDetails(
|
||||||
|
contactId,
|
||||||
|
{
|
||||||
|
enabled: !!contactId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
|
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(ContactSchema),
|
resolver: zodResolver(ContactSchema),
|
||||||
defaultValues: {
|
defaultValues: defaultContactValue,
|
||||||
name: "",
|
|
||||||
organization: "",
|
|
||||||
contactCategoryId: null,
|
|
||||||
address: "",
|
|
||||||
description: "",
|
|
||||||
designation: "",
|
|
||||||
projectIds: [],
|
|
||||||
contactEmails: [],
|
|
||||||
contactPhones: [],
|
|
||||||
tags: [],
|
|
||||||
bucketIds: [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -67,7 +51,6 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
getValues,
|
getValues,
|
||||||
trigger,
|
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
reset,
|
reset,
|
||||||
@ -87,38 +70,50 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
} = useFieldArray({ control, name: "contactPhones" });
|
} = useFieldArray({ control, name: "contactPhones" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emailFields.length === 0) {
|
if (contactId && contactData) {
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
reset({
|
||||||
}
|
name: contactData.name || "",
|
||||||
if (phoneFields.length === 0) {
|
description: contactData.description || "",
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
|
||||||
}
|
|
||||||
}, [emailFields.length, phoneFields.length]);
|
|
||||||
|
|
||||||
const handleAddEmail = async () => {
|
designation: contactData.designation || "",
|
||||||
const emails = getValues("contactEmails");
|
organization: contactData.organization || "",
|
||||||
const lastIndex = emails.length - 1;
|
|
||||||
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
|
contactEmails: contactData.contactEmails?.length
|
||||||
if (valid) {
|
? contactData.contactEmails
|
||||||
appendEmail({ label: "Work", emailAddress: "" });
|
: [{ 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 || [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
}, [contactId, contactData, reset]);
|
||||||
|
|
||||||
const handleAddPhone = async () => {
|
useEffect(() => {
|
||||||
const phones = getValues("contactPhones");
|
if (!contactId) {
|
||||||
const lastIndex = phones.length - 1;
|
if (emailFields.length === 0)
|
||||||
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
|
appendEmail({ label: "Work", emailAddress: "" });
|
||||||
if (valid) {
|
if (phoneFields.length === 0)
|
||||||
appendPhone({ label: "Office", phoneNumber: "" });
|
appendPhone({ label: "Office", phoneNumber: "" });
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
contactId,
|
||||||
|
emailFields.length,
|
||||||
|
phoneFields.length,
|
||||||
|
appendEmail,
|
||||||
|
appendPhone,
|
||||||
|
]);
|
||||||
|
|
||||||
const watchBucketIds = watch("bucketIds");
|
const watchBucketIds = watch("bucketIds") || [];
|
||||||
|
|
||||||
// handle logic when input of desgination is changed
|
|
||||||
const handleDesignationChange = (e) => {
|
const handleDesignationChange = (e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
|
|
||||||
const matches = designationList.filter((org) =>
|
const matches = designationList.filter((org) =>
|
||||||
org.toLowerCase().includes(val.toLowerCase())
|
org.toLowerCase().includes(val.toLowerCase())
|
||||||
);
|
);
|
||||||
@ -127,36 +122,29 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
setTimeout(() => setShowSuggestions(false), 5000);
|
setTimeout(() => setShowSuggestions(false), 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// handle logic when designation is selected
|
|
||||||
const handleSelectDesignation = (val) => {
|
const handleSelectDesignation = (val) => {
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
setValue("designation", val);
|
setValue("designation", val);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle phone number input to only allow numbers and max length of 10
|
// bucket toggle
|
||||||
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 });
|
|
||||||
};
|
|
||||||
const handleCheckboxChange = (id) => {
|
const handleCheckboxChange = (id) => {
|
||||||
const updated = watchBucketIds.includes(id)
|
const updated = watchBucketIds.includes(id)
|
||||||
? watchBucketIds.filter((i) => i !== id)
|
? watchBucketIds.filter((i) => i !== id)
|
||||||
: [...watchBucketIds, id];
|
: [...watchBucketIds, id];
|
||||||
|
|
||||||
setValue("bucketIds", updated, { shouldValidate: true });
|
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 onSubmit = (data) => {
|
||||||
const cleaned = {
|
const payload = {
|
||||||
...data,
|
...data,
|
||||||
contactEmails: (data.contactEmails || []).filter(
|
contactEmails: (data.contactEmails || []).filter(
|
||||||
(e) => e.emailAddress?.trim() !== ""
|
(e) => e.emailAddress?.trim() !== ""
|
||||||
@ -166,23 +154,45 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
setSubmitting(true);
|
if (contactId) {
|
||||||
submitContact(cleaned, reset, setSubmitting);
|
const contactPayload = { ...data, id: contactId };
|
||||||
|
UpdateContact({
|
||||||
|
contactId: contactData.id,
|
||||||
|
contactPayload: contactPayload,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
CreateContact(payload);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const orgValue = watch("organization");
|
|
||||||
|
|
||||||
const handleClosed = () => {
|
const handleClosed = () => {
|
||||||
onCLosed();
|
closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddEmail = () => {
|
||||||
|
appendEmail({ label: "Work", emailAddress: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPhone = () => {
|
||||||
|
appendPhone({ label: "Office", phoneNumber: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPending = updating || creating;
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="d-flex justify-content-center align-items-center">
|
<div className="d-flex justify-content-center align-items-center mb-4">
|
||||||
<h5 className="m-0 fw-18"> Create New Contact</h5>
|
<h5 className="m-0 fw-18">
|
||||||
|
{contactId ? "Edit Contact" : "Create New Contact"}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Name + Organization */}
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 text-start">
|
<div className="col-md-6 text-start">
|
||||||
<Label className="form-label" required>Name</Label>
|
<Label htmlFor={"name"} required>
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
@ -191,20 +201,26 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
<small className="danger-text">{errors.name.message}</small>
|
<small className="danger-text">{errors.name.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-6 text-start">
|
||||||
|
<Label htmlFor={"organization"} required>
|
||||||
|
Organization
|
||||||
|
</Label>
|
||||||
|
<InputSuggestions
|
||||||
|
organizationList={organizationList}
|
||||||
|
value={watch("organization") || ""}
|
||||||
|
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
|
||||||
|
error={errors.organization?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Organization</Label>
|
|
||||||
<InputSuggestions
|
|
||||||
organizationList={organizationList}
|
|
||||||
value={getValues("organization") || ""}
|
|
||||||
onChange={(val) => setValue("organization", val)}
|
|
||||||
error={errors.organization?.message}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Designation */}
|
||||||
<div className="row mt-1">
|
<div className="row mt-1">
|
||||||
<div className="col-md-6 text-start">
|
<div className="col-md-6 text-start position-relative">
|
||||||
<Label className="form-label" required>Designation</Label>
|
<Label htmlFor={"designation"} required>
|
||||||
|
Designation
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register("designation")}
|
{...register("designation")}
|
||||||
@ -218,26 +234,14 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
marginTop: "2px",
|
marginTop: "2px",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
borderRadius: "0px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredDesignationList.map((designation) => (
|
{filteredDesignationList.map((designation) => (
|
||||||
<li
|
<li
|
||||||
key={designation}
|
key={designation}
|
||||||
className="list-group-item list-group-item-action border-none "
|
className="list-group-item list-group-item-action"
|
||||||
style={{
|
style={{ cursor: "pointer", fontSize: "14px" }}
|
||||||
cursor: "pointer",
|
|
||||||
padding: "5px 12px",
|
|
||||||
fontSize: "14px",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
}}
|
|
||||||
onMouseDown={() => handleSelectDesignation(designation)}
|
onMouseDown={() => handleSelectDesignation(designation)}
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "#f8f9fa")
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "transparent")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{designation}
|
{designation}
|
||||||
</li>
|
</li>
|
||||||
@ -251,6 +255,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Emails + Phones */}
|
||||||
<div className="row mt-1">
|
<div className="row mt-1">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
{emailFields.map((field, index) => (
|
{emailFields.map((field, index) => (
|
||||||
@ -258,7 +264,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
key={field.id}
|
key={field.id}
|
||||||
className="row d-flex align-items-center mb-1"
|
className="row d-flex align-items-center mb-1"
|
||||||
>
|
>
|
||||||
<div className="col-5 text-start">
|
<div className="col-5 text-start">
|
||||||
<label className="form-label">Label</label>
|
<label className="form-label">Label</label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -268,13 +274,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
<option value="Personal">Personal</option>
|
<option value="Personal">Personal</option>
|
||||||
<option value="Other">Other</option>
|
<option value="Other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
{errors.contactEmails?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.contactEmails[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-7 text-start">
|
<div className="col-7 text-start">
|
||||||
<label className="form-label">Email</label>
|
<label className="form-label">Email</label>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<input
|
<input
|
||||||
@ -290,7 +291,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<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)}
|
onClick={() => removeEmail(index)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -304,13 +305,14 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
{phoneFields.map((field, index) => (
|
{phoneFields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className="row d-flex align-items-center mb-2"
|
className="row d-flex align-items-center mb-2"
|
||||||
>
|
>
|
||||||
<div className="col-5 text-start">
|
<div className="col-5 text-start">
|
||||||
<label className="form-label">Label</label>
|
<label className="form-label">Label</label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -320,22 +322,15 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
<option value="Personal">Personal</option>
|
<option value="Personal">Personal</option>
|
||||||
<option value="Business">Business</option>
|
<option value="Business">Business</option>
|
||||||
</select>
|
</select>
|
||||||
{errors.phone?.[index]?.label && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.ContactPhones[index].label.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-7 text-start">
|
<div className="col-7 text-start">
|
||||||
<label className="form-label">Phone</label>
|
<label className="form-label">Phone</label>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
{...register(`contactPhones.${index}.phoneNumber`)}
|
||||||
placeholder="9876543210"
|
placeholder="9876543210"
|
||||||
onInput={handlePhoneInput}
|
|
||||||
maxLength={10}
|
|
||||||
/>
|
/>
|
||||||
{index === phoneFields.length - 1 ? (
|
{index === phoneFields.length - 1 ? (
|
||||||
<i
|
<i
|
||||||
@ -344,7 +339,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<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)}
|
onClick={() => removePhone(index)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -358,13 +353,11 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{errors.contactPhone?.message && (
|
|
||||||
<div className="danger-text">{errors.contactPhone.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Category + Projects */}
|
||||||
<div className="row my-1">
|
<div className="row my-1">
|
||||||
<div className="col-md-6 text-start">
|
<div className="col-md-6 text-start">
|
||||||
<label className="form-label">Category</label>
|
<label className="form-label">Category</label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -408,24 +401,31 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<TagInput name="tags" label="Tags" options={contactTags} />
|
<TagInput
|
||||||
|
name="tags"
|
||||||
|
label="Tags"
|
||||||
|
options={contactTags}
|
||||||
|
isRequired={true}
|
||||||
|
/>
|
||||||
{errors.tags && (
|
{errors.tags && (
|
||||||
<small className="danger-text">{errors.tags.message}</small>
|
<small className="danger-text">{errors.tags.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Buckets */}
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-12 mt-1 text-start">
|
<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">
|
||||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
|
||||||
{bucketsLoaging && <p>Loading...</p>}
|
{bucketsLoaging && <p>Loading...</p>}
|
||||||
{buckets?.map((item) => (
|
{buckets?.map((item) => (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="list-inline-item flex-shrink-0 me-6 mb-1"
|
className="list-inline-item flex-shrink-0 me-6 mb-1"
|
||||||
>
|
>
|
||||||
<div className="form-check ">
|
<div className="form-check">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
@ -444,13 +444,12 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{errors.bucketIds && (
|
{errors.bucketIds && (
|
||||||
<small className="danger-text mt-0">
|
<small className="danger-text">{errors.bucketIds.message}</small>
|
||||||
{errors.bucketIds.message}
|
|
||||||
</small>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Address + Description */}
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<label className="form-label">Address</label>
|
<label className="form-label">Address</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -459,9 +458,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
{...register("address")}
|
{...register("address")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 text-start">
|
||||||
<div className="col-12 text-start mt-1">
|
<label className="form-label">Description</label>
|
||||||
<Label className="form-label" required>Description</Label>
|
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
rows="2"
|
rows="2"
|
||||||
@ -474,20 +472,21 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
|||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2 py-2 mt-3">
|
<div className="d-flex justify-content-end gap-2 py-2 mt-3">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-secondary"
|
className="btn btn-sm btn-label-secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClosed}
|
onClick={handleClosed}
|
||||||
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-sm btn-primary" type="submit">
|
<button className="btn btn-sm btn-primary" type="submit" disabled={isPending}>
|
||||||
{IsSubmitting ? "Please Wait..." : "Submit"}
|
{isPending ? "Please Wait..." : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ManageDirectory;
|
export default ManageContact;
|
@ -6,21 +6,15 @@ import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
|||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||||
import "../common/TextEditor/Editor.css";
|
import "../common/TextEditor/Editor.css";
|
||||||
|
import { useActiveInActiveNote } from "../../hooks/useDirectory";
|
||||||
|
|
||||||
const NoteCardDirectory = ({
|
const NoteCardDirectory = ({ noteItem, contactId }) => {
|
||||||
refetchProfile,
|
|
||||||
refetchNotes,
|
|
||||||
noteItem,
|
|
||||||
contactId,
|
|
||||||
setProfileContact,
|
|
||||||
}) => {
|
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [editorValue, setEditorValue] = useState(noteItem.note);
|
const [editorValue, setEditorValue] = useState(noteItem.note);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isActivProcess, setActiveProcessing] = useState(false);
|
const [isActivProcess, setActiveProcessing] = useState(false);
|
||||||
|
|
||||||
// State to manage hover status
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const handleUpdateNote = async () => {
|
const handleUpdateNote = async () => {
|
||||||
@ -36,12 +30,6 @@ const NoteCardDirectory = ({
|
|||||||
noteItem.id,
|
noteItem.id,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
setProfileContact((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notes: prev.notes.map((note) =>
|
|
||||||
note.id === noteItem.id ? response?.data : note
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cached_contactProfile = getCachedData("Contact Profile");
|
const cached_contactProfile = getCachedData("Contact Profile");
|
||||||
|
|
||||||
@ -81,10 +69,6 @@ const NoteCardDirectory = ({
|
|||||||
noteItem.id,
|
noteItem.id,
|
||||||
activeStatue
|
activeStatue
|
||||||
);
|
);
|
||||||
setProfileContact((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notes: prev.notes.filter((note) => note.id !== noteItem.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cachedContactProfile = getCachedData("Contact Profile");
|
const cachedContactProfile = getCachedData("Contact Profile");
|
||||||
|
|
||||||
@ -106,8 +90,6 @@ const NoteCardDirectory = ({
|
|||||||
}
|
}
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setActiveProcessing(false);
|
setActiveProcessing(false);
|
||||||
refetchNotes(contactId, false);
|
|
||||||
refetchProfile(contactId);
|
|
||||||
showToast(
|
showToast(
|
||||||
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
|
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
|
||||||
"success"
|
"success"
|
||||||
@ -121,6 +103,10 @@ const NoteCardDirectory = ({
|
|||||||
showToast(msg, "error");
|
showToast(msg, "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { mutate: handleActiveInActive, isPending } = useActiveInActiveNote(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
|
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
|
||||||
@ -167,10 +153,15 @@ const NoteCardDirectory = ({
|
|||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
{!isDeleting ? (
|
{!isPending ? (
|
||||||
<i
|
<i
|
||||||
className="bx bx-trash bx-sm me-1 text-secondary cursor-pointer"
|
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>
|
></i>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -181,12 +172,17 @@ const NoteCardDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : isActivProcess ? (
|
) : isPending ? (
|
||||||
<i className="bx bx-loader-alt bx-spin text-primary"></i>
|
<i className="bx bx-loader-alt bx-spin text-primary"></i>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className="bx bx-recycle me-1 text-primary cursor-pointer"
|
className="bx bx-recycle me-1 text-primary cursor-pointer"
|
||||||
onClick={() => handleDeleteNote(!noteItem?.isActive)}
|
onClick={() =>
|
||||||
|
handleActiveInActive({
|
||||||
|
noteId: noteItem?.id,
|
||||||
|
noteStatus: !noteItem?.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
title="Restore"
|
title="Restore"
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
|
@ -7,8 +7,8 @@ import showToast from "../../services/toastService";
|
|||||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||||
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
|
import ConfirmModal from "../common/ConfirmModal"; // Make sure path is correct
|
||||||
import "../common/TextEditor/Editor.css";
|
import "../common/TextEditor/Editor.css";
|
||||||
import ProfileContactDirectory from "./ProfileContactDirectory";
|
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
|
import { useActiveInActiveNote, useUpdateNote } from "../../hooks/useDirectory";
|
||||||
|
|
||||||
const NoteCardDirectoryEditable = ({
|
const NoteCardDirectoryEditable = ({
|
||||||
noteItem,
|
noteItem,
|
||||||
@ -25,55 +25,24 @@ const NoteCardDirectoryEditable = ({
|
|||||||
const [open_contact, setOpen_contact] = useState(null);
|
const [open_contact, setOpen_contact] = useState(null);
|
||||||
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
|
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
|
||||||
|
|
||||||
|
const { mutate: UpdateNote, isPending: isUpatingNote } = useUpdateNote(() =>
|
||||||
|
setEditing(false)
|
||||||
|
);
|
||||||
|
const { mutate: ActiveInactive, isPending: isUpdatingStatus } =
|
||||||
|
useActiveInActiveNote(() => setIsDeleteModalOpen(false));
|
||||||
|
|
||||||
const handleUpdateNote = async () => {
|
const handleUpdateNote = async () => {
|
||||||
try {
|
const payload = {
|
||||||
setIsLoading(true);
|
id: noteItem.id,
|
||||||
const payload = {
|
note: editorValue,
|
||||||
id: noteItem.id,
|
contactId,
|
||||||
note: editorValue,
|
};
|
||||||
contactId,
|
|
||||||
};
|
|
||||||
const response = await DirectoryRepository.UpdateNote(
|
|
||||||
noteItem.id,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
const cachedContactProfile = getCachedData("Contact Profile");
|
UpdateNote({ noteId: noteItem.id, notePayload: payload });
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const suspendEmployee = async () => {
|
const ActiveInActive = (noteItem) => {
|
||||||
try {
|
ActiveInactive({ noteId: noteItem.id, noteStatus: !noteItem.isActive });
|
||||||
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 contactProfile = (contactId) => {
|
const contactProfile = (contactId) => {
|
||||||
@ -98,24 +67,7 @@ const NoteCardDirectoryEditable = ({
|
|||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className="card shadow-sm border-1 mb-3 p-4 rounded"
|
className="card shadow-sm border-1 mb-3 p-4 rounded"
|
||||||
style={{
|
style={{
|
||||||
@ -187,12 +139,12 @@ const NoteCardDirectoryEditable = ({
|
|||||||
<div className="spinner-border spinner-border-sm text-danger" />
|
<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-loader-alt bx-spin text-primary"></i>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className="bx bx-recycle me-2 text-primary cursor-pointer"
|
className="bx bx-recycle me-2 text-primary cursor-pointer"
|
||||||
onClick={handleRestore}
|
onClick={() => ActiveInActive(noteItem)}
|
||||||
title="Restore"
|
title="Restore"
|
||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
@ -210,20 +162,25 @@ const NoteCardDirectoryEditable = ({
|
|||||||
theme="snow"
|
theme="snow"
|
||||||
className="compact-editor"
|
className="compact-editor"
|
||||||
/>
|
/>
|
||||||
<div className="d-flex justify-content-end gap-3 mt-2">
|
<div className="d-flex justify-content-end gap-0 mt-2">
|
||||||
<span
|
<button
|
||||||
className="text-secondary cursor-pointer"
|
type="button"
|
||||||
|
className="btn btn-sm btn-label-secondary me-2 px-2 py-1"
|
||||||
onClick={() => setEditing(false)}
|
onClick={() => setEditing(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</button>
|
||||||
<span
|
|
||||||
className="text-primary cursor-pointer"
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary px-2 py-1"
|
||||||
onClick={handleUpdateNote}
|
onClick={handleUpdateNote}
|
||||||
|
disabled={isUpatingNote}
|
||||||
>
|
>
|
||||||
{isLoading ? "Saving..." : "Submit"}
|
{isUpatingNote ? "Saving..." : "Submit"}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -233,19 +190,16 @@ const NoteCardDirectoryEditable = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete Confirm Modal */}
|
<ConfirmModal
|
||||||
{isDeleteModalOpen && (
|
type="delete"
|
||||||
<ConfirmModal
|
header="Delete Note"
|
||||||
isOpen={isDeleteModalOpen}
|
message="Are you sure you want to delete this note?"
|
||||||
type="delete"
|
onSubmit={ActiveInActive}
|
||||||
header="Delete Note"
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
message="Are you sure you want to delete this note?"
|
loading={isUpdatingStatus}
|
||||||
onSubmit={suspendEmployee}
|
paramData={noteItem}
|
||||||
onClose={() => setIsDeleteModalOpen(false)}
|
isOpen={isDeleteModalOpen}
|
||||||
loading={isDeleting}
|
/>
|
||||||
paramData={noteItem}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,8 +9,8 @@ const NotesCardViewDirectory = ({
|
|||||||
searchText,
|
searchText,
|
||||||
filterAppliedNotes,
|
filterAppliedNotes,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
|
|
||||||
const [allNotes, setAllNotes] = useState([]);
|
const [allNotes, setAllNotes] = useState([]);
|
||||||
const [filteredNotes, setFilteredNotes] = useState([]);
|
const [filteredNotes, setFilteredNotes] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -29,7 +29,7 @@ const NotesCardViewDirectory = ({
|
|||||||
const fetchNotes = async (projId) => {
|
const fetchNotes = async (projId) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await DirectoryRepository.GetNotes(1000, 1, projId); // ✅ pass projectId
|
const response = await DirectoryRepository.GetNotes(1000, 1, projId);
|
||||||
const fetchedNotes = response.data?.data || [];
|
const fetchedNotes = response.data?.data || [];
|
||||||
setAllNotes(fetchedNotes);
|
setAllNotes(fetchedNotes);
|
||||||
setNotesForFilter(fetchedNotes)
|
setNotesForFilter(fetchedNotes)
|
||||||
@ -116,11 +116,7 @@ const NotesCardViewDirectory = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100 h-100 ">
|
<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%" }}>
|
<div className="d-flex flex-column text-start" style={{ gap: "0rem", minHeight: "100%" }}>
|
||||||
{currentItems.map((noteItem) => (
|
{currentItems.map((noteItem) => (
|
||||||
<NoteCardDirectoryEditable
|
<NoteCardDirectoryEditable
|
||||||
@ -132,53 +128,12 @@ const NotesCardViewDirectory = ({
|
|||||||
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
|
prevNotes.map((n) => (n.id === updatedNote.id ? updatedNote : n))
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onNoteDelete={() => fetchNotes(projectId)} // ✅ reload with projectId
|
onNoteDelete={() => fetchNotes(projectId)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,45 +1,44 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Editor from "../common/TextEditor/Editor";
|
import Editor from "../common/TextEditor/Editor";
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/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 NoteCardDirectory from "./NoteCardDirectory";
|
||||||
import showToast from "../../services/toastService";
|
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({
|
const schema = z.object({
|
||||||
note: z.string().min(1, { message: "Note is required" }),
|
note: z.string().min(1, { message: "Note is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotesDirectory = ({
|
const NotesDirectory = ({ contactId,contactPerson }) => {
|
||||||
refetchProfile,
|
const [isActive, setIsActive] = useState(true);
|
||||||
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 [showEditor, setShowEditor] = useState(false);
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
|
reset,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: { note: "" },
|
||||||
note: "",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const noteValue = watch("note");
|
const noteValue = watch("note");
|
||||||
@ -48,116 +47,53 @@ const NotesDirectory = ({
|
|||||||
setValue("note", value, { shouldValidate: true });
|
setValue("note", value, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = (formData) => {
|
||||||
const newNote = { ...data, contactId: contactProfile?.id };
|
const notPayload = { ...formData, contactId };
|
||||||
try {
|
createNote(notPayload);
|
||||||
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 handleSwitch = () => {
|
const handleSwitch = () => {
|
||||||
setIsActive((prevIsActive) => {
|
setIsActive((prev) => !prev);
|
||||||
const newState = !prevIsActive;
|
|
||||||
refetch(contactProfile?.id, newState);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the fullName from contactProfile, which now includes middle and last names if available
|
const handleCancel =()=>{
|
||||||
const contactName =
|
reset()
|
||||||
contactProfile?.fullName || contactProfile?.firstName || "Contact";
|
setShowEditor(false)
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-start mt-10">
|
<div className="text-start mt-10">
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
<div className="row w-100 align-items-center">
|
<div className="row w-100 align-items-center">
|
||||||
{hasNotes && (
|
{data?.length > 0 && (
|
||||||
<div className="col col-2">
|
<div className="col col-2">
|
||||||
<p className="fw-semibold m-0 ms-3">Notes :</p>
|
<p className="fw-semibold m-0 ms-3">Notes :</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="col d-flex justify-content-end gap-2 pe-0">
|
<div className="col d-flex justify-content-end gap-2 pe-0">
|
||||||
{" "}
|
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
{/* Switch */}
|
||||||
<label
|
<label
|
||||||
className="switch switch-primary"
|
className="switch switch-primary"
|
||||||
style={{
|
style={{ fontSize: "15px" }}
|
||||||
fontSize: "15px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="switch-input"
|
className="switch-input"
|
||||||
onChange={handleSwitch}
|
onChange={handleSwitch}
|
||||||
checked={!IsActive} // invert binding
|
checked={!isActive} // invert binding
|
||||||
style={{ transform: "scale(0.8)" }}
|
style={{ transform: "scale(0.8)" }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="switch-toggle-slider"
|
className="switch-toggle-slider"
|
||||||
style={{
|
style={{ width: "30px", height: "15px" }}
|
||||||
width: "30px", // narrower slider
|
|
||||||
height: "15px", // shorter slider
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="switch-on"></span>
|
<span className="switch-on"></span>
|
||||||
<span className="switch-off"></span>
|
<span className="switch-off"></span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="switch-label"
|
className="switch-label"
|
||||||
style={{
|
style={{ fontSize: "14px", marginLeft: "-14px" }}
|
||||||
fontSize: "14px", // smaller label text
|
|
||||||
marginLeft: "-14px"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Include Deleted Notes
|
Include Deleted Notes
|
||||||
</span>
|
</span>
|
||||||
@ -189,22 +125,23 @@ const NotesDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Editor */}
|
||||||
{showEditor && (
|
{showEditor && (
|
||||||
<div className="card m-2 mb-5 position-relative">
|
<div className="card m-2 mb-5 position-relative">
|
||||||
<span
|
<span
|
||||||
type="button"
|
type="button"
|
||||||
className="position-absolute top-0 end-0 mt-3 bg-secondary rounded-circle"
|
className="position-absolute top-0 end-0 mt-3 bg-secondary rounded-circle"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
onClick={() => setShowEditor(false)}
|
onClick={() => setShowEditor(false)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-x fs-5 p-1 text-white"></i>
|
<i className="bx bx-x fs-5 p-1 text-white"></i>
|
||||||
</span>
|
</span>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Editor
|
<Editor
|
||||||
value={noteValue}
|
value={noteValue}
|
||||||
loading={IsSubmitting}
|
loading={isPending}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
onCancel={onCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
{errors.note && (
|
{errors.note && (
|
||||||
@ -214,32 +151,28 @@ const NotesDirectory = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className=" justify-content-start px-1 mt-1">
|
<div className="justify-content-start px-1 mt-1">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center">
|
<NoetCard/>
|
||||||
{" "}
|
|
||||||
<p>Loading...</p>{" "}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{!isLoading && notesToDisplay.length > 0
|
|
||||||
? notesToDisplay
|
{!isLoading && data?.length > 0
|
||||||
.slice()
|
? data.map((noteItem) => (
|
||||||
.reverse()
|
|
||||||
.map((noteItem) => (
|
|
||||||
<NoteCardDirectory
|
<NoteCardDirectory
|
||||||
refetchProfile={refetchProfile}
|
|
||||||
refetchNotes={refetch}
|
|
||||||
refetchContact={refetch}
|
|
||||||
noteItem={noteItem}
|
|
||||||
contactId={contactProfile?.id}
|
|
||||||
setProfileContact={setProfileContact}
|
|
||||||
key={noteItem.id}
|
key={noteItem.id}
|
||||||
|
noteItem={noteItem}
|
||||||
|
contactId={contactId}
|
||||||
|
// updateNote={updateNote}
|
||||||
|
// toggleNoteStatus={toggleNoteStatus}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: !isLoading &&
|
: !isLoading &&
|
||||||
!showEditor && (
|
!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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
|
@ -189,7 +189,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-xs"
|
className="btn btn-label-secondary btn-xs"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
|
@ -235,24 +235,9 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
<p className="fw-bold fs-6">Upload New Document</p>
|
<p className="fw-bold fs-6">Upload New Document</p>
|
||||||
<FormProvider key={documentTypeId} {...methods}>
|
<FormProvider key={documentTypeId} {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
|
||||||
{/* Document Name */}
|
|
||||||
<div className="mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Document Name
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<div className="danger-text">{errors.name.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label htmlFor="documentCategoryId">Document Category</Label>
|
<Label htmlFor="documentCategoryId" required>Document Category</Label>
|
||||||
<select
|
<select
|
||||||
{...register("documentCategoryId")}
|
{...register("documentCategoryId")}
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -279,7 +264,7 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
{/* Type */}
|
{/* Type */}
|
||||||
{categoryId && (
|
{categoryId && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label htmlFor="documentTypeId">Document Type</Label>
|
<Label htmlFor="documentTypeId" required>Document Type</Label>
|
||||||
<select
|
<select
|
||||||
{...register("documentTypeId")}
|
{...register("documentTypeId")}
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -303,14 +288,15 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Document ID */}
|
{/* Document ID */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label
|
<label
|
||||||
htmlFor="documentId"
|
htmlFor="documentId"
|
||||||
required={selectedType?.isMandatory ?? false}
|
required={selectedType?.isMandatory ?? false}
|
||||||
>
|
>
|
||||||
Document ID
|
Document ID
|
||||||
</Label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@ -321,6 +307,23 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Document Name */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Document Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("name")}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<div className="danger-text">{errors.name.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Upload */}
|
{/* Upload */}
|
||||||
<div className="row my-2">
|
<div className="row my-2">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import DateRangePicker from "../common/DateRangePicker";
|
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
|
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
|
||||||
import usePagination from "../../hooks/usePagination";
|
import usePagination from "../../hooks/usePagination";
|
||||||
@ -11,13 +11,32 @@ import AttendLogs from "../Activities/AttendLogs";
|
|||||||
import { useAttendanceByEmployee } from "../../hooks/useAttendance";
|
import { useAttendanceByEmployee } from "../../hooks/useAttendance";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
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 EmpAttendance = ({ employee }) => {
|
||||||
const [attendances, setAttendnaces] = useState([]);
|
const [attendances, setAttendnaces] = useState([]);
|
||||||
const [selectedDate, setSelectedDate] = useState("");
|
const [selectedDate, setSelectedDate] = useState("");
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [attendanceId, setAttendanecId] = useState();
|
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 {
|
const {
|
||||||
data = [],
|
data = [],
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
@ -25,15 +44,13 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useAttendanceByEmployee(employee, dateRange.startDate, dateRange.endDate);
|
} = useAttendanceByEmployee(
|
||||||
|
employee,
|
||||||
|
localToUtc(startDate),
|
||||||
|
localToUtc(endDate)
|
||||||
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// const { data, loading, error } = useSelector(
|
|
||||||
// (store) => store.employeeAttendance
|
|
||||||
// );
|
|
||||||
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(true);
|
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@ -71,13 +88,6 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group5 = data.filter((d) => d.activity === 5).sort(sortByName);
|
const group5 = data.filter((d) => d.activity === 5).sort(sortByName);
|
||||||
|
|
||||||
// const sortedFinalList = [
|
|
||||||
// ...group1,
|
|
||||||
// ...group2,
|
|
||||||
// ...group3,
|
|
||||||
// ...group4,
|
|
||||||
// ...group5,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const uniqueMap = new Map();
|
const uniqueMap = new Map();
|
||||||
|
|
||||||
@ -114,6 +124,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
};
|
};
|
||||||
const closeModal = () => setIsModalOpen(false);
|
const closeModal = () => setIsModalOpen(false);
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
@ -126,12 +137,22 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||||
id="DataTables_Table_0_length"
|
id="DataTables_Table_0_length"
|
||||||
>
|
>
|
||||||
<div className="col-md-4 my-0 ">
|
<div className="col-3 my-0 ">
|
||||||
<DateRangePicker
|
<>
|
||||||
DateDifference="7"
|
<FormProvider {...methods}>
|
||||||
onRangeChange={setDateRange}
|
<form
|
||||||
endDateMode="today"
|
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>
|
||||||
<div className="col-md-2 m-0 text-end">
|
<div className="col-md-2 m-0 text-end">
|
||||||
<i
|
<i
|
||||||
|
@ -34,9 +34,13 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<div className="avatar flex-shrink-0 me-3">
|
<div className="avatar flex-shrink-0 me-3">
|
||||||
<span className="avatar-initial rounded bg-label-primary">
|
<span
|
||||||
|
className={`avatar-initial rounded ${project.removedDate ? "bg-label-warning" : "bg-label-success"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<i className="icon-base bx bx-buildings icon-lg"></i>
|
<i className="icon-base bx bx-buildings icon-lg"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 className="mb-0">{project.projectShortName}</h6>
|
<h6 className="mb-0">{project.projectShortName}</h6>
|
||||||
@ -49,6 +53,14 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
<em>NA</em>
|
<em>NA</em>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{project.removedDate && (
|
||||||
|
<div className="mt-0 d-flex flex-column flex-sm-row justify-content-between">
|
||||||
|
<div className="label-secondary">
|
||||||
|
Unassigned:{" "}
|
||||||
|
{new Date(project.removedDate).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -59,14 +71,7 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dates */}
|
{/* Dates */}
|
||||||
{project.removedDate && (
|
|
||||||
<div className="mt-2 d-flex flex-column flex-sm-row justify-content-between">
|
|
||||||
<div className="label-secondary">
|
|
||||||
Unassigned:{" "}
|
|
||||||
{new Date(project.removedDate).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -187,7 +187,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-xs"
|
className="btn btn-label-secondary btn-xs"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
|
@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||||
import { formatFileSize } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
@ -183,9 +183,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
const onSubmit = (fromdata) => {
|
const onSubmit = (fromdata) => {
|
||||||
let payload = {
|
let payload = {
|
||||||
...fromdata,
|
...fromdata,
|
||||||
transactionDate: moment
|
transactionDate: localToUtc(fromdata.transactionDate)
|
||||||
.utc(fromdata.transactionDate, "DD-MM-YYYY")
|
|
||||||
.toISOString(),
|
|
||||||
};
|
};
|
||||||
if (expenseToEdit) {
|
if (expenseToEdit) {
|
||||||
const editPayload = { ...payload, id: data.id };
|
const editPayload = { ...payload, id: data.id };
|
||||||
@ -331,7 +330,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<Label htmlFor="transactionDate" className="form-label" required>
|
<Label htmlFor="transactionDate" className="form-label" required>
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</Label>
|
</Label>
|
||||||
<DatePicker name="transactionDate" control={control} />
|
<DatePicker name="transactionDate" control={control} maxDate={new Date()}/>
|
||||||
|
|
||||||
{errors.transactionDate && (
|
{errors.transactionDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
|
@ -47,6 +47,20 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// Close dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const infoRef = useRef(null);
|
const infoRef = useRef(null);
|
||||||
const infoRef1 = useRef(null);
|
const infoRef1 = useRef(null);
|
||||||
|
|
||||||
@ -255,19 +269,20 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
<div className="row mb-1">
|
<div className="row mb-1">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form-text text-start">
|
<div className="form-text text-start">
|
||||||
<div className="d-flex align-items-center form-text fs-7">
|
<div
|
||||||
|
className="d-flex align-items-center form-text fs-7"
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
<span className="text-dark">Select Team</span>
|
<span className="text-dark">Select Team</span>
|
||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
<div className="dropdown position-relative d-inline-block">
|
<div className="dropdown position-relative d-inline-block">
|
||||||
<a
|
<a
|
||||||
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
|
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
|
||||||
? "text-secondary"
|
? "text-secondary"
|
||||||
: "text-primary"
|
: "text-primary"
|
||||||
}`}
|
}`}
|
||||||
data-bs-toggle="dropdown"
|
onClick={() => setOpen(!open)}
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
>
|
||||||
<i className="bx bx-slider-alt ms-2"></i>
|
<i className="bx bx-slider-alt ms-2"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -290,57 +305,55 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdown Menu with Scroll */}
|
{/* Dropdown Menu */}
|
||||||
<ul
|
{open && (
|
||||||
className="dropdown-menu p-2 text-capitalize"
|
<ul
|
||||||
style={{ maxHeight: "300px", overflowY: "auto" }}
|
className="dropdown-menu show p-2 text-capitalize"
|
||||||
>
|
style={{ maxHeight: "300px", overflowY: "auto" }}
|
||||||
{/* All Roles */}
|
>
|
||||||
<li key="all">
|
{/* All Roles */}
|
||||||
<div className="form-check dropdown-item py-0">
|
<li key="all">
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="checkboxAllRoles"
|
|
||||||
value="all"
|
|
||||||
checked={selectedRoles.includes("all")}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleRoleChange(e, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label ms-2"
|
|
||||||
htmlFor="checkboxAllRoles"
|
|
||||||
>
|
|
||||||
All Roles
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{/* Dynamic Roles */}
|
|
||||||
{jobRolesForDropdown?.map((role) => (
|
|
||||||
<li key={role.id}>
|
|
||||||
<div className="form-check dropdown-item py-0">
|
<div className="form-check dropdown-item py-0">
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={`checkboxRole-${role.id}`}
|
id="checkboxAllRoles"
|
||||||
value={role.id}
|
value="all"
|
||||||
checked={selectedRoles.includes(String(role.id))}
|
checked={selectedRoles.includes("all")}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleRoleChange(e, e.target.value)}
|
||||||
handleRoleChange(e, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="form-check-label ms-2"
|
className="form-check-label ms-2"
|
||||||
htmlFor={`checkboxRole-${role.id}`}
|
htmlFor="checkboxAllRoles"
|
||||||
>
|
>
|
||||||
{role.name}
|
All Roles
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
|
||||||
</ul>
|
{/* Dynamic Roles */}
|
||||||
|
{jobRolesForDropdown?.map((role) => (
|
||||||
|
<li key={role.id}>
|
||||||
|
<div className="form-check dropdown-item py-0">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id={`checkboxRole-${role.id}`}
|
||||||
|
value={role.id}
|
||||||
|
checked={selectedRoles.includes(String(role.id))}
|
||||||
|
onChange={(e) => handleRoleChange(e, e.target.value)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label ms-2"
|
||||||
|
htmlFor={`checkboxRole-${role.id}`}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Box */}
|
{/* Search Box */}
|
||||||
|
@ -9,7 +9,7 @@ import ProjectRepository, {
|
|||||||
TasksRepository,
|
TasksRepository,
|
||||||
} from "../../repositories/ProjectRepository";
|
} from "../../repositories/ProjectRepository";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
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 InfraTable from "./Infrastructure/InfraTable";
|
||||||
import {
|
import {
|
||||||
cacheData,
|
cacheData,
|
||||||
@ -34,6 +34,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
|
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
|
||||||
const [ project, setProject ] = useState( projects_Details );
|
const [ project, setProject ] = useState( projects_Details );
|
||||||
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
|
const ManageTask = useHasUserPermission(MANAGE_TASK)
|
||||||
const [showModalFloor, setshowModalFloor] = useState(false);
|
const [showModalFloor, setshowModalFloor] = useState(false);
|
||||||
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
|
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
|
||||||
const [showModalTask, setshowModalTask] = useState(false);
|
const [showModalTask, setshowModalTask] = useState(false);
|
||||||
@ -87,13 +88,12 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
<div className="align-items-center">
|
<div className="align-items-center">
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
<div
|
<div
|
||||||
className={`col-12 text-end mb-1 ${
|
className={`col-12 text-end mb-1 `}
|
||||||
!ManageInfra && "d-none"
|
|
||||||
} `}
|
|
||||||
>
|
>
|
||||||
<button
|
{ManageInfra && (<>
|
||||||
|
<button
|
||||||
type="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)}
|
onClick={()=>setshowModalBuilding(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
@ -101,7 +101,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={()=>setshowModalFloor(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
@ -109,20 +109,21 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setshowModalWorkArea(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Manage Work Areas
|
Manage Work Areas
|
||||||
</button>
|
</button></>)}
|
||||||
<button
|
|
||||||
|
{(ManageTask || ManageInfra) && (<button
|
||||||
type="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)}
|
onClick={()=>setshowModalTask(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Create Tasks
|
Create Tasks
|
||||||
</button>
|
</button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
|
@ -5,14 +5,26 @@ import {
|
|||||||
DIRECTORY_ADMIN,
|
DIRECTORY_ADMIN,
|
||||||
DIRECTORY_MANAGER,
|
DIRECTORY_MANAGER,
|
||||||
DIRECTORY_USER,
|
DIRECTORY_USER,
|
||||||
|
MANAGE_PROJECT_INFRA,
|
||||||
|
MANAGE_TASK,
|
||||||
|
MANAGE_TEAM,
|
||||||
|
MODIFY_DOCUMENT,
|
||||||
|
UPLOAD_DOCUMENT,
|
||||||
|
VIEW_DOCUMENT,
|
||||||
VIEW_PROJECT_INFRA,
|
VIEW_PROJECT_INFRA,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
|
|
||||||
const ProjectNav = ({ onPillClick, activePill }) => {
|
const ProjectNav = ({ onPillClick, activePill }) => {
|
||||||
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
||||||
|
const HasManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
|
const HasManageTask = useHasUserPermission(MANAGE_TASK)
|
||||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
||||||
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
||||||
|
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
|
||||||
|
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT);
|
||||||
|
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
|
||||||
|
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
|
||||||
|
|
||||||
const ProjectTab = [
|
const ProjectTab = [
|
||||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||||
@ -21,7 +33,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
key: "infra",
|
key: "infra",
|
||||||
icon: "bx bx-grid-alt",
|
icon: "bx bx-grid-alt",
|
||||||
label: "Infrastructure",
|
label: "Infrastructure",
|
||||||
hidden: !HasViewInfraStructure,
|
hidden: !(HasViewInfraStructure || HasManageInfra || HasManageTask),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "directory",
|
key: "directory",
|
||||||
@ -29,8 +41,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
label: "Directory",
|
label: "Directory",
|
||||||
hidden: !(DirAdmin || DireManager || DirUser),
|
hidden: !(DirAdmin || DireManager || DirUser),
|
||||||
},
|
},
|
||||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents" },
|
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting" },
|
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="nav-align-top">
|
<div className="nav-align-top">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
useProjectLevelEmployeePermission,
|
useProjectLevelEmployeePermission,
|
||||||
useProjectLevelModules,
|
useProjectLevelModules,
|
||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from "../../hooks/useProjects";
|
} from "../../hooks/useProjects";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
@ -27,7 +27,8 @@ const ProjectPermission = () => {
|
|||||||
watch,
|
watch,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors },
|
control,
|
||||||
|
formState: { errors, isDirty },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(ProjectPermissionSchema),
|
resolver: zodResolver(ProjectPermissionSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -44,18 +45,18 @@ const ProjectPermission = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!employees.length) return;
|
if (!selectedEmployee) return;
|
||||||
|
|
||||||
const enabledPerms =
|
const enabledPerms =
|
||||||
selectedEmpPermissions?.permissions
|
selectedEmpPermissions?.permissions
|
||||||
?.filter((perm) => perm.isEnabled)
|
?.filter((perm) => perm.isEnabled)
|
||||||
?.map((perm) => perm.id) || [];
|
?.map((perm) => perm.id) || [];
|
||||||
|
|
||||||
reset({
|
reset((prev) => ({
|
||||||
employeeId: selectedEmployee || employees[0]?.id || "",
|
...prev,
|
||||||
selectedPermissions: enabledPerms,
|
selectedPermissions: enabledPerms,
|
||||||
});
|
}));
|
||||||
}, [selectedEmpPermissions, reset, selectedEmployee, employees]);
|
}, [selectedEmpPermissions, reset, selectedEmployee]);
|
||||||
|
|
||||||
const { mutate: updatePermission, isPending } =
|
const { mutate: updatePermission, isPending } =
|
||||||
useUpdateProjectLevelEmployeePermission();
|
useUpdateProjectLevelEmployeePermission();
|
||||||
@ -67,30 +68,23 @@ const ProjectPermission = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
||||||
|
const existingEnabledIds = existingPermissions
|
||||||
|
.filter((p) => p.isEnabled)
|
||||||
|
.map((p) => p.id);
|
||||||
|
|
||||||
const payloadPermissions =
|
const newSelectedIds = formData.selectedPermissions || [];
|
||||||
existingPermissions.length > 0
|
|
||||||
? existingPermissions.map((perm) => ({
|
const added = newSelectedIds
|
||||||
id: perm.id,
|
.filter((id) => !existingEnabledIds.includes(id))
|
||||||
isEnabled: formData.selectedPermissions?.includes(perm.id) || false,
|
.map((id) => ({ id, isEnabled: true }));
|
||||||
}))
|
|
||||||
: (formData.selectedPermissions || []).map((id) => ({
|
const removed = existingEnabledIds
|
||||||
id,
|
.filter((id) => !newSelectedIds.includes(id))
|
||||||
isEnabled: true,
|
.map((id) => ({ id, isEnabled: false }));
|
||||||
}));
|
|
||||||
|
const payloadPermissions = [...added, ...removed];
|
||||||
|
|
||||||
if (payloadPermissions.length === 0) {
|
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");
|
showToast("No changes detected", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,76 +95,116 @@ const ProjectPermission = () => {
|
|||||||
permission: payloadPermissions,
|
permission: payloadPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Final payload:", payload);
|
||||||
updatePermission(payload);
|
updatePermission(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row px-2 py-1">
|
<div className="w-100 p py-1 ">
|
||||||
|
<div className="text-start m-0">
|
||||||
|
<p className="fw-semibold fs-6">Project Permission</p>
|
||||||
|
</div>
|
||||||
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
||||||
{/* Employee Dropdown */}
|
<div className="d-flex justify-content-between align-items-end gap-2 mb-3">
|
||||||
<div className="d-flex align-items-end gap-2">
|
<div className="text-start d-flex align-items-center gap-2">
|
||||||
<div className="text-start">
|
<div className="d-block">
|
||||||
<label className="form-label">Select Employee</label>
|
<label className="form-label">Select Employee</label>
|
||||||
<select
|
</div>
|
||||||
className="form-select form-select-sm"
|
<div className="d-block">
|
||||||
{...register("employeeId")}
|
{" "}
|
||||||
disabled={isPending}
|
<select
|
||||||
>
|
className="form-select form-select-sm"
|
||||||
{loading ? (
|
{...register("employeeId")}
|
||||||
<option value="">Loading...</option>
|
disabled={isPending}
|
||||||
) : (
|
>
|
||||||
<>
|
{loading ? (
|
||||||
<option value="">-- Select --</option>
|
<option value="">Loading...</option>
|
||||||
{employees.map((emp) => (
|
) : (
|
||||||
<option key={emp.id} value={emp.id}>
|
<>
|
||||||
{emp.firstName} {emp.lastName}
|
<option value="">-- Select Employee --</option>
|
||||||
</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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{errors.employeeId && (
|
||||||
|
<div className="d-block text-danger small">
|
||||||
|
{errors.employeeId.message}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</select>
|
</div>
|
||||||
{errors.employeeId && (
|
</div>
|
||||||
<div className="text-danger small">
|
|
||||||
{errors.employeeId.message}
|
<div className="mt-3 text-end">
|
||||||
</div>
|
{isDirty && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={isPending || loading}
|
||||||
|
>
|
||||||
|
{isPending ? "Please Wait..." : "Save Permission"}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-sm btn-primary" disabled={isPending || loading}>
|
|
||||||
{isPending ? "Please Wait..." : "Update Permission"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Permissions */}
|
|
||||||
{ProjectModules.map((feature) => (
|
{ProjectModules.map((feature) => (
|
||||||
<div key={feature.id} className="row my-2 px-3 ">
|
<div
|
||||||
<div className="col-12 text-start fw-semibold mb-2">
|
key={feature.id}
|
||||||
{feature.name}
|
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
|
||||||
</div>
|
>
|
||||||
<div className="col-12">
|
<div className="card text-start border-1 p-1">
|
||||||
<div className="row">
|
<p className="card-title fs-6 fw-semibold">{feature.name}</p>
|
||||||
{feature.featurePermissions?.map((perm) => (
|
<div className="px-2">
|
||||||
<div
|
<ul className="list-unstyled">
|
||||||
className="col-12 col-sm-6 col-md-4 mb-2"
|
{feature.featurePermissions?.map((perm) => (
|
||||||
key={perm.id}
|
<div className="d-flex my-2" key={perm.id}>
|
||||||
>
|
<Controller
|
||||||
<label
|
name="selectedPermissions"
|
||||||
className="form-check-label d-flex align-items-center"
|
control={control}
|
||||||
htmlFor={perm.id}
|
render={({ field }) => {
|
||||||
>
|
const value = field.value || [];
|
||||||
<input
|
const isChecked = value.includes(perm.id);
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input me-2"
|
return (
|
||||||
id={perm.id}
|
<label
|
||||||
value={perm.id}
|
className="form-check-label d-flex align-items-center"
|
||||||
{...register("selectedPermissions")}
|
htmlFor={perm.id}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input me-2"
|
||||||
|
id={perm.id}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
field.onChange([...value, perm.id]); // add
|
||||||
|
} else {
|
||||||
|
field.onChange(
|
||||||
|
value.filter((v) => v !== perm.id)
|
||||||
|
); // remove
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{perm.name}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{perm.name}
|
</div>
|
||||||
</label>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-2" />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</form>
|
</form>
|
||||||
|
@ -3,9 +3,10 @@ import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
|||||||
import ProjectPermission from "./ProjectPermission";
|
import ProjectPermission from "./ProjectPermission";
|
||||||
|
|
||||||
const ProjectSetting = () => {
|
const ProjectSetting = () => {
|
||||||
const [activePill, setActivePill] = useState(() => {
|
const [activePill, setActivePill] = useState("Permissions")
|
||||||
return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
// const [activePill, setActivePill] = useState(() => {
|
||||||
});
|
// return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
||||||
|
// });
|
||||||
const projectSettingTab = [
|
const projectSettingTab = [
|
||||||
{ key: "Permissions", label: "Permissions" },
|
{ key: "Permissions", label: "Permissions" },
|
||||||
{ key: "Notification", label: "Notification" },
|
{ key: "Notification", label: "Notification" },
|
||||||
@ -32,8 +33,8 @@ const ProjectSetting = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-100">
|
<div className="w-100">
|
||||||
<div className="card py-2 px-5">
|
<div className="card py-2 px-5">
|
||||||
<div className="col-4">
|
{/* <div className="col-12">
|
||||||
<div className="dropdown text-start">
|
<div className="dropdown text-end">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
@ -63,7 +64,7 @@ const ProjectSetting = () => {
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="mt-3">{renderContent()}</div>
|
<div className="mt-3">{renderContent()}</div>
|
||||||
</div>
|
</div>
|
||||||
|