Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Landing_pages
This commit is contained in:
commit
ae3258cb04
@ -114,7 +114,10 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table-responsive text-nowrap h-100" >
|
||||
<div
|
||||
className="table-responsive text-nowrap h-100"
|
||||
style={{ minHeight: "200px" }} // 🔹 Ensures fixed height
|
||||
>
|
||||
<div className="d-flex text-start align-items-center py-2">
|
||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||
<div className="form-check form-switch text-start m-0 ms-5">
|
||||
@ -209,7 +212,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
||||
</tr>
|
||||
))}
|
||||
{!attendance && (
|
||||
<span className="text-secondary m-4">No employees assigned to the project!</span>
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center text-secondary" style={{ height: "200px" }}>
|
||||
No employees assigned to the project!
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -258,7 +265,10 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted my-4">
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-muted"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: attendanceList.length === 0
|
||||
|
@ -42,7 +42,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPending,setShowPending] = useState(false)
|
||||
const [showPending, setShowPending] = useState(false)
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [processedData, setProcessedData] = useState([]);
|
||||
@ -245,17 +245,16 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
</div>
|
||||
<div className="col-md-2 m-0 text-end">
|
||||
<i
|
||||
className={`bx bx-refresh cursor-pointer fs-4 ${
|
||||
isFetching ? "spin" : ""
|
||||
}`}
|
||||
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
|
||||
}`}
|
||||
title="Refresh"
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap">
|
||||
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
||||
{isLoading ? (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
||||
<p className="text-secondary">Loading...</p>
|
||||
</div>
|
||||
) : filteredSearchData?.length > 0 ? (
|
||||
@ -284,9 +283,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
const previousAttendance = arr[index - 1];
|
||||
const previousDate = previousAttendance
|
||||
? moment(
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
previousAttendance.checkInTime ||
|
||||
previousAttendance.checkOutTime
|
||||
).format("YYYY-MM-DD")
|
||||
: null;
|
||||
|
||||
if (!previousDate || currentDate !== previousDate) {
|
||||
@ -346,12 +345,15 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="my-4"><span className="text-secondary">No Record Available !</span></div>
|
||||
<div className="my-4"><span className="text-secondary">No Record Available !</span></div>
|
||||
)}
|
||||
</div>
|
||||
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
||||
<div className="my-4">
|
||||
<span className="text-secondary">No Pending Record Available !</span>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center text-secondary"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
No Record Available !
|
||||
</div>
|
||||
)}
|
||||
{filteredSearchData.length > ITEMS_PER_PAGE && (
|
||||
@ -369,9 +371,8 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
(pageNumber) => (
|
||||
<li
|
||||
key={pageNumber}
|
||||
className={`page-item ${
|
||||
currentPage === pageNumber ? "active" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === pageNumber ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
@ -383,9 +384,8 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
||||
)
|
||||
)}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
|
@ -98,15 +98,16 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center flex-wrap">
|
||||
<div className="col-12 d-flex justify-content-center">
|
||||
<label className="fs-5 text-dark text-center">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
? "Check-out :"
|
||||
: "Check-in :"}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-6 col-md-6 ">
|
||||
|
||||
<div className="col-6 col-md-6 text-start">
|
||||
<label className="form-label" htmlFor="checkInDate">
|
||||
{modeldata?.checkInTime && !modeldata?.checkOutTime
|
||||
? "Check-out Date"
|
||||
@ -125,7 +126,7 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-6 col-md-6">
|
||||
<div className="col-6 col-md-6 text-start">
|
||||
<TimePicker
|
||||
label="Choose a time"
|
||||
onChange={(e) => setValue("markTime", e)}
|
||||
@ -138,7 +139,7 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<label className="form-label" htmlFor="description">
|
||||
Description
|
||||
</label>
|
||||
@ -154,19 +155,19 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<div className="col-12 text-end mt-4">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={() => closeModal()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -87,9 +87,9 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
||||
}, [employeeHandler]);
|
||||
|
||||
return (
|
||||
<div className="table-responsive text-nowrap pb-4">
|
||||
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
|
||||
{loading ? (
|
||||
<div className="my-2">
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
|
||||
<p className="text-secondary">Loading...</p>
|
||||
</div>
|
||||
) : currentItems?.length > 0 ? (
|
||||
@ -143,9 +143,14 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="my-4">
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: "200px" }}
|
||||
>
|
||||
<span className="text-secondary">
|
||||
{searchTerm ? "No results found for your search." : "No Requests Found !"}
|
||||
{searchTerm
|
||||
? "No results found for your search."
|
||||
: "No Requests Found !"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@ -163,9 +168,8 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${
|
||||
currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
@ -176,9 +180,8 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${
|
||||
currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link "
|
||||
|
@ -185,21 +185,22 @@ export const ReportTask = ({ report, closeModal }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 text-center my-2">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={isPending}>
|
||||
{isPending ? "Please wait" : "Submit Report"}
|
||||
</button>
|
||||
<div className="col-12 text-end my-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
onClick={handleClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isPending}>
|
||||
{isPending ? "Please wait" : "Submit Report"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ReportTask;
|
||||
export default ReportTask;
|
@ -10,6 +10,7 @@ import { getBgClassFromHash } from "../../utils/projectStatus";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import ImagePreview from "../common/ImagePreview";
|
||||
import { useAuditStatus, useSubmitTaskComment } from "../../hooks/useTasks";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const ReportTaskComments = ({
|
||||
commentsData,
|
||||
@ -291,10 +292,10 @@ const ReportTaskComments = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-6 col-sm-4 text-center align-items-end m-0">
|
||||
<label htmlFor="workStatus" className="form-label">
|
||||
<div className="col-6 col-sm-4 text-start align-items-end m-0">
|
||||
<Label htmlFor="workStatus" className="form-label" required>
|
||||
Audit Status
|
||||
</label>
|
||||
</Label>
|
||||
<select
|
||||
id="workStatus"
|
||||
className={`form-select form-select-sm`}
|
||||
@ -361,7 +362,7 @@ const ReportTaskComments = ({
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
className="btn btn-sm btn-label-secondary mt-5"
|
||||
onClick={closeModal}
|
||||
data-bs-dismiss="modal"
|
||||
disabled={isPending}
|
||||
@ -370,7 +371,7 @@ const ReportTaskComments = ({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary ms-2"
|
||||
className="btn btn-sm btn-primary ms-2 mt-5"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending
|
||||
|
@ -98,42 +98,42 @@ const AttendanceOverview = () => {
|
||||
colors: roles.map((_, i) => flatColors[i % flatColors.length]),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white p-4 rounded shadow d-flex flex-column"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1">Attendance Overview</h5>
|
||||
<p className="card-subtitle">Role-wise present count</p>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={dayRange}
|
||||
onChange={(e) => setDayRange(Number(e.target.value))}
|
||||
>
|
||||
<option value={7}>Last 7 Days</option>
|
||||
<option value={15}>Last 15 Days</option>
|
||||
<option value={30}>Last 30 Days</option>
|
||||
</select>
|
||||
<button
|
||||
className={`btn btn-sm ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`}
|
||||
onClick={() => setView("chart")}
|
||||
title="Chart View"
|
||||
>
|
||||
<i className="bx bx-bar-chart-alt-2"></i>
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${view === "table" ? "btn-primary" : "btn-outline-primary"}`}
|
||||
onClick={() => setView("table")}
|
||||
title="Table View"
|
||||
>
|
||||
<i className="bx bx-task text-success"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className="bg-white p-4 rounded shadow d-flex flex-column"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1 fw-bold">Attendance Overview</h5>
|
||||
<p className="card-subtitle">Role-wise present count</p>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={dayRange}
|
||||
onChange={(e) => setDayRange(Number(e.target.value))}
|
||||
>
|
||||
<option value={7}>Last 7 Days</option>
|
||||
<option value={15}>Last 15 Days</option>
|
||||
<option value={30}>Last 30 Days</option>
|
||||
</select>
|
||||
<button
|
||||
className={`btn btn-sm ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`}
|
||||
onClick={() => setView("chart")}
|
||||
title="Chart View"
|
||||
>
|
||||
<i className="bx bx-bar-chart-alt-2"></i>
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${view === "table" ? "btn-primary" : "btn-outline-primary"}`}
|
||||
onClick={() => setView("table")}
|
||||
title="Table View"
|
||||
>
|
||||
<i className="bx bx-task text-success"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-grow-1 d-flex align-items-center justify-content-center">
|
||||
|
@ -19,7 +19,7 @@ const ProjectCompletionChart = () => {
|
||||
<div className="card h-100">
|
||||
<div className="card-header d-flex align-items-start justify-content-between">
|
||||
<div className="card-title mb-0 text-start">
|
||||
<h5 className="mb-1">Projects</h5>
|
||||
<h5 className="mb-1 fw-bold ">Projects</h5>
|
||||
<p className="card-subtitle">Projects Completion Status</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -90,7 +90,7 @@ const ProjectProgressChart = ({
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
||||
{/* Left: Title */}
|
||||
<div className="card-title text-start">
|
||||
<h5 className="mb-1">Project Progress</h5>
|
||||
<h5 className="mb-1 fw-bold">Project Progress</h5>
|
||||
<p className="card-subtitle">Progress Overview by Project</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -198,23 +198,14 @@ const ManageBucket = () => {
|
||||
return (
|
||||
<>
|
||||
{deleteBucket && (
|
||||
<div
|
||||
className={`modal fade ${deleteBucket ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: deleteBucket ? "block" : "none",
|
||||
backgroundColor: deleteBucket ? "rgba(0,0,0,0.5)" : "transparent",
|
||||
}}
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Bucket"}
|
||||
message={"Are you sure you want to delete this bucket?"}
|
||||
onSubmit={handleDeleteContact}
|
||||
onClose={() => setDeleteBucket(null)}
|
||||
/>
|
||||
</div>
|
||||
<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" }}>
|
||||
@ -237,8 +228,9 @@ const ManageBucket = () => {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<i
|
||||
className={`bx bx-refresh cursor-pointer fs-4 ${loading ? "spin" : ""
|
||||
}`}
|
||||
className={`bx bx-refresh cursor-pointer fs-4 ${
|
||||
loading ? "spin" : ""
|
||||
}`}
|
||||
title="Refresh"
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
@ -247,8 +239,9 @@ const ManageBucket = () => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm btn-primary ms-auto ${action_bucket ? "d-none" : ""
|
||||
}`}
|
||||
className={`btn btn-sm btn-primary ms-auto ${
|
||||
action_bucket ? "d-none" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setAction_bucket(true);
|
||||
select_bucket(null);
|
||||
@ -285,16 +278,18 @@ const ManageBucket = () => {
|
||||
</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.
|
||||
{!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>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{!loading &&
|
||||
sortedBucktesList.map((bucket) => (
|
||||
<div className="col" key={bucket.id}>
|
||||
@ -305,29 +300,29 @@ const ManageBucket = () => {
|
||||
{(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
|
||||
)
|
||||
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>
|
||||
)}
|
||||
)
|
||||
.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:{" "}
|
||||
|
@ -23,6 +23,7 @@ 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 ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
const selectedMaster = useSelector(
|
||||
@ -40,7 +41,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
const { designationList, loading: designloading } = useDesignation();
|
||||
const { contactTags, loading: Tagloading } = useContactTags();
|
||||
const [IsSubmitting, setSubmitting] = useState(false);
|
||||
const [showSuggestions,setShowSuggestions] = useState(false);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@ -132,6 +133,12 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
setValue("designation", val);
|
||||
};
|
||||
|
||||
// Handle phone number input to only allow numbers and max length of 10
|
||||
const handlePhoneInput = (e) => {
|
||||
const value = e.target.value.replace(/[^0-9]/g, "");
|
||||
e.target.value = value.slice(0, 10);
|
||||
};
|
||||
|
||||
|
||||
const toggleBucketId = (id) => {
|
||||
const updated = watchBucketIds?.includes(id)
|
||||
@ -171,11 +178,11 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
<FormProvider {...methods}>
|
||||
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<h6 className="m-0 fw-18"> Create New Contact</h6>
|
||||
<h5 className="m-0 fw-18"> Create New Contact</h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-6 text-start">
|
||||
<label className="form-label">Name</label>
|
||||
<Label className="form-label" required>Name</Label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
{...register("name")}
|
||||
@ -186,7 +193,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 text-start">
|
||||
<label className="form-label">Organization</label>
|
||||
<Label className="form-label" required>Organization</Label>
|
||||
<InputSuggestions
|
||||
organizationList={organizationList}
|
||||
value={getValues("organization") || ""}
|
||||
@ -197,7 +204,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
</div>
|
||||
<div className="row mt-1">
|
||||
<div className="col-md-6 text-start">
|
||||
<label className="form-label">Designation</label>
|
||||
<Label className="form-label" required>Designation</Label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
{...register("designation")}
|
||||
@ -277,23 +284,11 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{index === emailFields.length - 1 ? (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-xs btn-primary ms-1"
|
||||
// onClick={handleAddEmail}
|
||||
// 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-primary"
|
||||
onClick={() => removeEmail(index)}
|
||||
@ -335,29 +330,19 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
<label className="form-label">Phone</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="text"
|
||||
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-danager"
|
||||
onClick={() => removePhone(index)}
|
||||
@ -431,7 +416,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 mt-1 text-start">
|
||||
<label className="form-label ">Select Bucket</label>
|
||||
<Label className="form-label" required>Select Bucket</Label>
|
||||
|
||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
||||
{bucketsLoaging && <p>Loading...</p>}
|
||||
@ -475,8 +460,8 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Description</label>
|
||||
<div className="col-12 text-start mt-1">
|
||||
<Label className="form-label" required>Description</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
@ -487,10 +472,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-center gap-1 py-2">
|
||||
<button className="btn btn-sm btn-primary" type="submit">
|
||||
{IsSubmitting ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<div className="d-flex justify-content-end gap-2 py-2 mt-3">
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
type="button"
|
||||
@ -498,10 +480,14 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-sm btn-primary" type="submit">
|
||||
{IsSubmitting ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDirectory;
|
||||
export default ManageDirectory;
|
@ -33,7 +33,10 @@ const NoteCardDirectoryEditable = ({
|
||||
note: editorValue,
|
||||
contactId,
|
||||
};
|
||||
const response = await DirectoryRepository.UpdateNote(noteItem.id, payload);
|
||||
const response = await DirectoryRepository.UpdateNote(
|
||||
noteItem.id,
|
||||
payload
|
||||
);
|
||||
|
||||
const cachedContactProfile = getCachedData("Contact Profile");
|
||||
if (cachedContactProfile?.contactId === contactId) {
|
||||
@ -75,9 +78,9 @@ const NoteCardDirectoryEditable = ({
|
||||
|
||||
const contactProfile = (contactId) => {
|
||||
DirectoryRepository.GetContactProfile(contactId).then((res) => {
|
||||
setOpen_contact(res?.data);
|
||||
setIsOpenModalNote(true);
|
||||
});
|
||||
setOpen_contact(res?.data);
|
||||
setIsOpenModalNote(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestore = async () => {
|
||||
@ -95,7 +98,6 @@ const NoteCardDirectoryEditable = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{isOpenModalNote && (
|
||||
<GlobalModel
|
||||
isOpen={isOpenModalNote}
|
||||
@ -125,7 +127,6 @@ const NoteCardDirectoryEditable = ({
|
||||
{/* Header */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||
<div className="d-flex align-items-center">
|
||||
|
||||
<Avatar
|
||||
size="xxs"
|
||||
firstName={noteItem?.createdBy?.firstName}
|
||||
@ -133,30 +134,36 @@ const NoteCardDirectoryEditable = ({
|
||||
className="m-0"
|
||||
/>
|
||||
<div>
|
||||
<div className="d-flex ms-0 align-middle cursor-pointer" onClick={() =>contactProfile(noteItem.contactId)}>
|
||||
<div
|
||||
className="d-flex ms-0 align-middle cursor-pointer"
|
||||
onClick={() => contactProfile(noteItem.contactId)}
|
||||
>
|
||||
<span>
|
||||
<span className="fw-bold "> {noteItem?.contactName} </span> <span className="text-muted font-weight-normal">
|
||||
<span className="fw-bold "> {noteItem?.contactName} </span>{" "}
|
||||
<span className="text-muted font-weight-normal">
|
||||
({noteItem?.organizationName})
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div className="d-flex ms-0 align-middle">
|
||||
|
||||
|
||||
</div>
|
||||
<div className="d-flex ms-0 align-middle"></div>
|
||||
<div className="d-flex ms-0 mt-2">
|
||||
<span className="text-muted">
|
||||
by <span className="fw-bold "> {noteItem?.createdBy?.firstName} {noteItem?.createdBy?.lastName} </span>
|
||||
<span className="text-muted">
|
||||
on {moment
|
||||
by{" "}
|
||||
<span className="fw-bold ">
|
||||
{" "}
|
||||
{noteItem?.createdBy?.firstName}{" "}
|
||||
{noteItem?.createdBy?.lastName}{" "}
|
||||
</span>
|
||||
{" "}
|
||||
<span className="text-muted">
|
||||
on{" "}
|
||||
{moment
|
||||
.utc(noteItem?.createdAt)
|
||||
.add(5, "hours")
|
||||
.add(30, "minutes")
|
||||
.format("DD MMMM, YYYY [at] hh:mm A")}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,26 +235,16 @@ const NoteCardDirectoryEditable = ({
|
||||
|
||||
{/* Delete Confirm Modal */}
|
||||
{isDeleteModalOpen && (
|
||||
<div
|
||||
className={`modal fade ${isDeleteModalOpen ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: isDeleteModalOpen ? "block" : "none",
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Note"}
|
||||
message={"Are you sure you want to delete this note?"}
|
||||
onSubmit={suspendEmployee}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={isDeleting}
|
||||
paramData={noteItem}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Note"
|
||||
message="Are you sure you want to delete this note?"
|
||||
onSubmit={suspendEmployee}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={isDeleting}
|
||||
paramData={noteItem}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -128,42 +128,40 @@ const NotesDirectory = ({
|
||||
<div className="col d-flex justify-content-end gap-2 pe-0">
|
||||
{" "}
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
|
||||
<label
|
||||
className="switch switch-primary"
|
||||
|
||||
<label
|
||||
className="switch switch-primary"
|
||||
style={{
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="switch-input"
|
||||
onChange={() => handleSwitch(!IsActive)}
|
||||
checked={IsActive}
|
||||
style={{
|
||||
transform: "scale(0.8)", // smaller toggle
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="switch-toggle-slider"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="switch-input"
|
||||
onChange={handleSwitch}
|
||||
checked={!IsActive} // invert binding
|
||||
style={{ transform: "scale(0.8)" }}
|
||||
/>
|
||||
<span
|
||||
className="switch-toggle-slider"
|
||||
style={{
|
||||
width: "30px", // narrower slider
|
||||
height: "15px", // shorter slider
|
||||
}}
|
||||
>
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span
|
||||
className="switch-label"
|
||||
>
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span
|
||||
className="switch-label"
|
||||
style={{
|
||||
fontSize: "14px", // smaller label text
|
||||
marginLeft: "-14px"
|
||||
}}
|
||||
>
|
||||
Include Deleted Notes
|
||||
</span>
|
||||
</label>
|
||||
>
|
||||
Include Deleted Notes
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{!showEditor && (
|
||||
<div className="d-flex justify-content-end">
|
||||
@ -225,23 +223,23 @@ const NotesDirectory = ({
|
||||
)}
|
||||
{!isLoading && notesToDisplay.length > 0
|
||||
? notesToDisplay
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((noteItem) => (
|
||||
<NoteCardDirectory
|
||||
refetchProfile={refetchProfile}
|
||||
refetchNotes={refetch}
|
||||
refetchContact={refetch}
|
||||
noteItem={noteItem}
|
||||
contactId={contactProfile?.id}
|
||||
setProfileContact={setProfileContact}
|
||||
key={noteItem.id}
|
||||
/>
|
||||
))
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((noteItem) => (
|
||||
<NoteCardDirectory
|
||||
refetchProfile={refetchProfile}
|
||||
refetchNotes={refetch}
|
||||
refetchContact={refetch}
|
||||
noteItem={noteItem}
|
||||
contactId={contactProfile?.id}
|
||||
setProfileContact={setProfileContact}
|
||||
key={noteItem.id}
|
||||
/>
|
||||
))
|
||||
: !isLoading &&
|
||||
!showEditor && (
|
||||
<div className="text-center mt-5">{noNotesMessage}</div>
|
||||
)}
|
||||
<div className="text-center mt-5">{noNotesMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -23,6 +23,7 @@ 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(
|
||||
@ -121,6 +122,11 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
||||
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) => {
|
||||
@ -208,7 +214,7 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
||||
<FormProvider {...methods}>
|
||||
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<h6 className="m-0 fw-18"> Update Contact</h6>
|
||||
<h5 className="m-0 fw-18"> Update Contact</h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-6 text-start">
|
||||
@ -239,7 +245,7 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
||||
</div>
|
||||
<div className="row mt-1">
|
||||
<div className="col-md-6 text-start">
|
||||
<label className="form-label">Designation</label>
|
||||
<Label className="form-label" required>Designation</Label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
{...register("designation")}
|
||||
@ -377,10 +383,12 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
||||
<label className="form-label">Phone</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="text"
|
||||
type="tel"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
||||
placeholder="9876543210"
|
||||
onInput={handlePhoneInput}
|
||||
maxLength={10}
|
||||
/>
|
||||
{index === phoneFields.length - 1 ? (
|
||||
// <button
|
||||
@ -529,22 +537,22 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-center gap-1 py-2">
|
||||
<div className="d-flex justify-content-end gap-2 py-0 mt-4">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
type="submit"
|
||||
disabled={IsSubmitting}
|
||||
>
|
||||
{IsSubmitting ? "Please Wait..." : "Update"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
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>
|
||||
|
88
src/components/Documents/DocumentDetailsSkeleton .jsx
Normal file
88
src/components/Documents/DocumentDetailsSkeleton .jsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import VersionListSkeleton from "./VersionListSkeleton";
|
||||
|
||||
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
|
||||
<div
|
||||
className={`skeleton mb-2 ${className}`}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
|
||||
const DocumentDetailsSkeleton = () => {
|
||||
return (
|
||||
<div className="p-1">
|
||||
<p className="fw-bold fs-6">Document Details</p>
|
||||
|
||||
{/* Row 1 */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="60%" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="50%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="40%" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="60%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="40%" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="130px" />
|
||||
<SkeletonLine width="50%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Row 6 - Description */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12">
|
||||
<div className="d-flex">
|
||||
|
||||
<SkeletonLine width="100%" height={40} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version list skeleton */}
|
||||
<div className="row text-start py-2">
|
||||
<VersionListSkeleton items={2} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentDetailsSkeleton;
|
206
src/components/Documents/DocumentFilterPanel.jsx
Normal file
206
src/components/Documents/DocumentFilterPanel.jsx
Normal file
@ -0,0 +1,206 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDocumentFilterEntities } from "../../hooks/useDocument";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DocumentFilterDefaultValues,
|
||||
DocumentFilterSchema,
|
||||
} from "./DocumentSchema";
|
||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import moment from "moment";
|
||||
|
||||
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
const { data, isError, isLoading, error } =
|
||||
useDocumentFilterEntities(entityTypeId);
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(DocumentFilterSchema),
|
||||
defaultValues: DocumentFilterDefaultValues,
|
||||
});
|
||||
|
||||
const { handleSubmit, reset, setValue, watch } = methods;
|
||||
|
||||
// Watch values from form
|
||||
const isUploadedAt = watch("isUploadedAt");
|
||||
const isVerified = watch("isVerified");
|
||||
|
||||
// Close the offcanvas (bootstrap specific)
|
||||
const closePanel = () => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
const onSubmit = (values) => {
|
||||
onApply({
|
||||
...values,
|
||||
startDate: values.startDate
|
||||
? moment.utc(values.startDate, "DD-MM-YYYY").toISOString()
|
||||
: null,
|
||||
endDate: values.endDate
|
||||
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
||||
: null,
|
||||
});
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
reset(DocumentFilterDefaultValues);
|
||||
setResetKey((prev) => prev + 1);
|
||||
onApply(DocumentFilterDefaultValues);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (isError)
|
||||
return <div>Error: {error?.message || "Something went wrong!"}</div>;
|
||||
|
||||
const {
|
||||
uploadedBy = [],
|
||||
documentCategory = [],
|
||||
documentType = [],
|
||||
documentTag = [],
|
||||
} = data?.data || {};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Date Range Section */}
|
||||
<div className="mb-2">
|
||||
<div className="text-start d-flex align-items-center my-1">
|
||||
<label className="form-label me-2 my-0">Choose Date:</label>
|
||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isUploadedAt", true)}
|
||||
>
|
||||
Uploaded On
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||
!isUploadedAt ? "active btn-secondary text-white" : ""
|
||||
}`}
|
||||
onClick={() => setValue("isUploadedAt", false)}
|
||||
>
|
||||
Updated On
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DateRangePicker1
|
||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||
startField="startDate"
|
||||
endField="endDate"
|
||||
defaultRange={true}
|
||||
resetSignal={resetKey}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Filters */}
|
||||
<div className="row g-2 text-start">
|
||||
<SelectMultiple
|
||||
name="uploadedByIds"
|
||||
label="Uploaded By:"
|
||||
options={uploadedBy}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="documentCategoryIds"
|
||||
label="Document Category:"
|
||||
options={documentCategory}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="documentTypeIds"
|
||||
label="Document Type:"
|
||||
options={documentType}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="documentTagIds"
|
||||
label="Tags:"
|
||||
options={documentTag}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="text-start my-2">
|
||||
<label className="form-label d-block mb-2">Choose Status:</label>
|
||||
<div className="d-flex gap-4">
|
||||
<label className="switch switch-sm">
|
||||
<input
|
||||
type="radio"
|
||||
className="switch-input"
|
||||
name="isVerified"
|
||||
checked={isVerified === null}
|
||||
onChange={() => setValue("isVerified", null)}
|
||||
/>
|
||||
<span className="switch-toggle-slider">
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span className="switch-label">All</span>
|
||||
</label>
|
||||
|
||||
<label className="switch switch-sm">
|
||||
<input
|
||||
type="radio"
|
||||
className="switch-input"
|
||||
name="isVerified"
|
||||
checked={isVerified === true}
|
||||
onChange={() => setValue("isVerified", true)}
|
||||
/>
|
||||
<span className="switch-toggle-slider">
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span className="switch-label">Verified</span>
|
||||
</label>
|
||||
|
||||
<label className="switch switch-sm">
|
||||
<input
|
||||
type="radio"
|
||||
className="switch-input"
|
||||
name="isVerified"
|
||||
checked={isVerified === false}
|
||||
onChange={() => setValue("isVerified", false)}
|
||||
/>
|
||||
<span className="switch-toggle-slider">
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span className="switch-label">Rejected</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Buttons */}
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-xs"
|
||||
onClick={onClear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-xs">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentFilterPanel;
|
132
src/components/Documents/DocumentSchema.js
Normal file
132
src/components/Documents/DocumentSchema.js
Normal file
@ -0,0 +1,132 @@
|
||||
import { z } from "zod";
|
||||
import { normalizeAllowedContentTypes } from "../../utils/appUtils";
|
||||
|
||||
export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => {
|
||||
const allowedTypes = normalizeAllowedContentTypes(allowedContentType);
|
||||
|
||||
return z.object({
|
||||
fileName: z.string().min(1, { message: "File name is required" }),
|
||||
base64Data: z.string().min(1, { message: "File data is required" }),
|
||||
contentType: z
|
||||
.string()
|
||||
.min(1, { message: "MIME type is required" })
|
||||
.refine(
|
||||
(val) => (allowedTypes.length ? allowedTypes.includes(val) : true),
|
||||
{
|
||||
message: `File type must be one of: ${allowedTypes.join(", ")}`,
|
||||
}
|
||||
),
|
||||
fileSize: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative("fileSize must be ≥ 0")
|
||||
.max(
|
||||
(maxSizeAllowedInMB ?? 25) * 1024 * 1024,
|
||||
`fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB`
|
||||
),
|
||||
description: z.string().optional().default(""),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
};
|
||||
|
||||
export const TagSchema = z.object({
|
||||
name: z.string().min(1, "Tag name is required"),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const DocumentPayloadSchema = (docConfig = {}) => {
|
||||
const {
|
||||
isMandatory,
|
||||
regexExpression,
|
||||
allowedContentType,
|
||||
maxSizeAllowedInMB,
|
||||
isUpdateForm,
|
||||
} = docConfig;
|
||||
|
||||
let documentIdSchema = z.string();
|
||||
|
||||
if (isMandatory) {
|
||||
documentIdSchema = documentIdSchema.min(1, {
|
||||
message: "DocumentId is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (regexExpression) {
|
||||
documentIdSchema = documentIdSchema.regex(
|
||||
new RegExp(regexExpression),
|
||||
"Invalid DocumentId format"
|
||||
);
|
||||
}
|
||||
|
||||
// Base attachment schema
|
||||
let attachmentSchema = AttachmentSchema(
|
||||
allowedContentType,
|
||||
maxSizeAllowedInMB
|
||||
).nullable();
|
||||
|
||||
// If not update form, require attachment
|
||||
if (!isUpdateForm) {
|
||||
attachmentSchema = attachmentSchema.refine((val) => val !== null, {
|
||||
message: "Attachment is required",
|
||||
});
|
||||
}
|
||||
|
||||
return z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
documentId: documentIdSchema,
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
documentTypeId: z
|
||||
.string()
|
||||
.min(1, { message: "Please Select Document Type" }),
|
||||
documentCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Please Select Document Category" }),
|
||||
attachment: attachmentSchema,
|
||||
tags: z.array(TagSchema).optional().default([]),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const defaultDocumentValues = {
|
||||
name: "",
|
||||
documentId: "",
|
||||
description: "",
|
||||
// entityId: "",
|
||||
documentTypeId: "",
|
||||
documentCategoryId: "",
|
||||
// attachment: {
|
||||
// fileName: "",
|
||||
// base64Data: "",
|
||||
// contentType: "",
|
||||
// fileSize: 0,
|
||||
// description: "",
|
||||
// isActive: true,
|
||||
// },
|
||||
attachment:null,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
|
||||
//--------------------Filter-------------------------
|
||||
|
||||
export const DocumentFilterSchema = z.object({
|
||||
uploadedByIds: z.array(z.string()).default([]),
|
||||
documentCategoryIds: z.array(z.string()).default([]),
|
||||
documentTypeIds: z.array(z.string()).default([]),
|
||||
documentTagIds: z.array(z.string()).default([]),
|
||||
isUploadedAt: z.boolean().default(true),
|
||||
isVerified: z.boolean().nullable().optional(),
|
||||
startDate: z.string().nullable().optional(),
|
||||
endDate: z.string().nullable().optional(),
|
||||
});
|
||||
export const DocumentFilterDefaultValues = {
|
||||
uploadedByIds: [],
|
||||
documentCategoryIds: [],
|
||||
documentTypeIds: [],
|
||||
documentTagIds: [],
|
||||
isUploadedAt: true,
|
||||
isVerified: null,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
};
|
||||
|
70
src/components/Documents/DocumentSkeleton.jsx
Normal file
70
src/components/Documents/DocumentSkeleton.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
|
||||
const SkeletonCell = ({
|
||||
width = "100%",
|
||||
height = 20,
|
||||
className = "",
|
||||
style = {},
|
||||
}) => (
|
||||
<div
|
||||
className={`skeleton ${className}`}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
borderRadius: 4,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DocumentTableSkeleton = ({ rows = 5 }) => {
|
||||
return (
|
||||
|
||||
<table className="card-body table border-top dataTable no-footer dtr-column text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-start">Name</th>
|
||||
<th className="text-start">Document Type</th>
|
||||
<th className="text-start">Uploaded By</th>
|
||||
<th className="text-center">Uploaded on</th>
|
||||
<th className="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{[...Array(rows)].map((_, idx) => (
|
||||
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
|
||||
{/* Name */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="120px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Document Type */}
|
||||
<td className="text-start">
|
||||
<SkeletonCell width="100px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Uploaded By (Avatar + Name) */}
|
||||
<td className="text-start">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<SkeletonCell width="30px" height={30} className="rounded-circle" />
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Uploaded on */}
|
||||
<td className="text-center">
|
||||
<SkeletonCell width="80px" height={16} />
|
||||
</td>
|
||||
|
||||
{/* Status */}
|
||||
<td className="text-center">
|
||||
<SkeletonCell width="70px" height={20} className="rounded" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
);
|
||||
};
|
287
src/components/Documents/DocumentVersionList.jsx
Normal file
287
src/components/Documents/DocumentVersionList.jsx
Normal file
@ -0,0 +1,287 @@
|
||||
import React from "react";
|
||||
import VersionListSkeleton from "./VersionListSkeleton";
|
||||
import { getDocuementsStatus } from "./Documents";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { DOWNLOAD_DOCUMENT, VERIFY_DOCUMENT } from "../../utils/constants";
|
||||
import { FileIcon } from "../../utils/FileIcon";
|
||||
|
||||
const DocumentVersionList = ({
|
||||
versionLoding,
|
||||
versionList,
|
||||
isPending,
|
||||
setOpenDocument,
|
||||
VerifyDocument,
|
||||
RejectDocument,
|
||||
showVersions,
|
||||
latestDoc,
|
||||
}) => {
|
||||
const canVerifyDocument = useHasUserPermission(VERIFY_DOCUMENT);
|
||||
const canDownloadDocument = useHasUserPermission(DOWNLOAD_DOCUMENT);
|
||||
|
||||
const handleOpenDocument = () => {
|
||||
if (canDownloadDocument) {
|
||||
setOpenDocument(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (versionLoding) {
|
||||
return <VersionListSkeleton items={2} />;
|
||||
}
|
||||
|
||||
const sortedVersions = versionList?.data
|
||||
? [...versionList.data].sort((a, b) => b.version - a.version)
|
||||
: [];
|
||||
|
||||
const currentDoc = sortedVersions.length ? sortedVersions[0] : latestDoc;
|
||||
|
||||
if (!showVersions) {
|
||||
if (!currentDoc) {
|
||||
return <p className="text-muted">No documents available.</p>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="list-group list-group-flush" style={{ marginTop: "-4px", marginLeft: "10px" }}>
|
||||
<div className="list-group-item list-group-item-action cursor-pointer" style={{ marginLeft: "-30px" }}>
|
||||
<div className="d-flex justify-content-between">
|
||||
{/* Left Side: Document Details */}
|
||||
<div className="d-flex align-items-start" onClick={handleOpenDocument}>
|
||||
<FileIcon
|
||||
type={currentDoc.contentType}
|
||||
size="fs-2"
|
||||
className="me-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
<span className="fw-semibold" style={{ marginTop: "-6px" }}>
|
||||
{currentDoc.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
{getDocuementsStatus(currentDoc.isVerified)}
|
||||
<span className="text-secondary text-tiny ms-7 ">
|
||||
File Size: {currentDoc.fileSize} Kb
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<small className="text-secondary fw-semibold me-2">
|
||||
Uploaded by
|
||||
</small>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={currentDoc.uploadedBy?.firstName}
|
||||
lastName={currentDoc.uploadedBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1">
|
||||
<small className="fw-normal" style={{ marginLeft: "-10px" }}>
|
||||
{`${currentDoc.uploadedBy?.firstName ?? ""} ${currentDoc.uploadedBy?.lastName ?? ""}`.trim() || "N/A"}
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
<small className="ms-2" style={{ marginRight: "-113px" }}>
|
||||
{formatUTCToLocalTime(currentDoc?.uploadedAt)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column align-items-end" style={{ marginTop: "-8px", marginRight: "-540px" }}>
|
||||
<span className="badge bg-success-subtle text-success">
|
||||
latest
|
||||
</span>
|
||||
|
||||
</div>
|
||||
{/* Right Side: Status and Info */}
|
||||
<div className="d-flex flex-column align-items-end" style={{ marginTop: "31px", marginRight: "21px" }}>
|
||||
{/* <span className="badge bg-success-subtle text-success">
|
||||
latest
|
||||
</span> */}
|
||||
<div className="d-flex align-items-center mt-1">
|
||||
{currentDoc?.updatedBy && (
|
||||
<>
|
||||
<small className="text-secondary me-2 fw-semibold">Updated by</small>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={currentDoc.updatedBy?.firstName}
|
||||
lastName={currentDoc.updatedBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1" style={{ marginRight: "-37px" }}>
|
||||
<small className="fw-normal" style={{ marginLeft: "-10px" }}>
|
||||
{`${currentDoc.updatedBy?.firstName ?? ""} ${currentDoc.updatedBy?.lastName ?? ""}`.trim() || "N/A"}
|
||||
</small>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Conditionally render updated date */}
|
||||
{currentDoc?.updatedAt && (
|
||||
<small className="ms-2" style={{ marginRight: "-75px" }}>
|
||||
{formatUTCToLocalTime(currentDoc?.updatedAt)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Buttons Div - only for the latest pending document when showVersions is false */}
|
||||
{currentDoc.isVerified === null && canVerifyDocument && (
|
||||
<div className="d-flex justify-content-end mb-3 me-4">
|
||||
{isPending ? (
|
||||
<span className="text-muted">Please Wait...</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={RejectDocument}
|
||||
className="btn btn-sm btn-danger me-2"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={VerifyDocument}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// If showVersions is true, display all versions
|
||||
if (!sortedVersions.length) {
|
||||
return <p className="text-muted">No documents available.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="list-group list-group-flush">
|
||||
{sortedVersions.map((document, index) => (
|
||||
<div
|
||||
key={document.id}
|
||||
className="list-group-item list-group-item-action cursor-pointer" style={{ marginLeft: "-30px",marginTop:"5px" }}
|
||||
>
|
||||
<div className="d-flex justify-content-between">
|
||||
|
||||
<div className="d-flex align-items-start" onClick={handleOpenDocument}>
|
||||
<FileIcon
|
||||
type={document.contentType}
|
||||
size="fs-2"
|
||||
className="me-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
<span className="fw-semibold" style={{ marginTop: "-6px" }}>
|
||||
{document.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
{getDocuementsStatus(document.isVerified)}
|
||||
<span className="ms-7 ">
|
||||
File Size: {document.fileSize} Kb
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<small className="text-secondary fw-semibold me-2">
|
||||
Uploaded by
|
||||
</small>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={document.uploadedBy?.firstName}
|
||||
lastName={document.uploadedBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1">
|
||||
<small className="fw-normal" style={{ marginLeft: "-10px" }}>
|
||||
{`${document.uploadedBy?.firstName ?? ""} ${document.uploadedBy?.lastName ?? ""}`.trim() || "N/A"}
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
<small className="ms-2" style={{ marginRight: "-113px" }}>
|
||||
{formatUTCToLocalTime(document?.uploadedAt)}
|
||||
</small>
|
||||
{document?.verifiedAt && (
|
||||
<div className="d-flex align-items-center mt-1">
|
||||
<small className="text-secondary me-2 fw-semibold">
|
||||
{document.isVerified ? "Approved by" : "Rejected by"}
|
||||
</small>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={document.verifiedBy?.firstName}
|
||||
lastName={document.verifiedBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1">
|
||||
<small className="fw-normal" style={{ marginLeft: "-10px" }}>
|
||||
{`${document.verifiedBy?.firstName ?? ""} ${document.verifiedBy?.lastName ?? ""}`.trim() || "N/A"}
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Conditionally render verified date */}
|
||||
{document?.verifiedAt && (
|
||||
<small className="ms-2" style={{ marginRight: "-103px" }}>
|
||||
{formatUTCToLocalTime(document?.verifiedAt)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-end" style={{ marginTop: "-13px", marginRight: "46px" }}>
|
||||
<small className="badge rounded-pill bg-label-secondary mt-1" style={{ marginRight: "-336px" }}>
|
||||
version {document.version}
|
||||
</small>
|
||||
{document.isLatest && (
|
||||
<span className="badge bg-success-subtle text-success">
|
||||
latest
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Right Side: Status and Actions */}
|
||||
<div className="d-flex flex-column align-items-end" style={{ marginTop: "16px", marginRight: "46px" }}>
|
||||
{/* <small className="badge rounded-pill bg-label-secondary mt-1" style={{ marginRight: "-105px" }}>
|
||||
version {document.version}
|
||||
</small> */}
|
||||
{document.isLatest && (
|
||||
<span className="badge bg-success-subtle text-success">
|
||||
latest
|
||||
</span>
|
||||
)}
|
||||
<div className="d-flex align-items-center mt-1">
|
||||
{document?.updatedBy && (
|
||||
<>
|
||||
<small className="text-secondary me-2 fw-semibold">Updated by</small>
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={document.updatedBy?.firstName}
|
||||
lastName={document.updatedBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1" style={{ marginRight: "-37px" }}>
|
||||
<small className="fw-normal" style={{ marginLeft: "-10px" }}>
|
||||
{`${document.updatedBy?.firstName ?? ""} ${document.updatedBy?.lastName ?? ""}`.trim() || "N/A"}
|
||||
</small>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Conditionally render updated date */}
|
||||
{document?.updatedAt && (
|
||||
<small className="ms-2" style={{ marginRight: "-75px" }}>
|
||||
{formatUTCToLocalTime(document?.updatedAt)}
|
||||
</small>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentVersionList;
|
27
src/components/Documents/DocumentViewerModal.jsx
Normal file
27
src/components/Documents/DocumentViewerModal.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDocumentVersion } from "../../hooks/useDocument";
|
||||
import { useDocumentContext } from "./Documents";
|
||||
import { error } from "pdf-lib";
|
||||
|
||||
const DocumentViewerModal = () => {
|
||||
const { viewDoc,setOpenDocument } = useDocumentContext();
|
||||
const { data, isLoading, isError,error } = useDocumentVersion(viewDoc.document);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.data) {
|
||||
const fileUrl = data.data;
|
||||
window.open(fileUrl, "_blank");
|
||||
setOpenDocument(false)
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) return <p>Loading document...</p>;
|
||||
if (isError) return <div>
|
||||
<p className="danger-text">{error.message}</p>
|
||||
</div>;
|
||||
|
||||
// Nothing to render inside modal since we redirect
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DocumentViewerModal;
|
250
src/components/Documents/Documents.jsx
Normal file
250
src/components/Documents/Documents.jsx
Normal file
@ -0,0 +1,250 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import GlobalModel from "../common/GlobalModel";
|
||||
import NewDocument from "./ManageDocument";
|
||||
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
||||
import { useParams } from "react-router-dom";
|
||||
import DocumentsList from "./DocumentsList";
|
||||
import DocumentFilterPanel from "./DocumentFilterPanel";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
DocumentFilterDefaultValues,
|
||||
DocumentFilterSchema,
|
||||
} from "./DocumentSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ManageDocument from "./ManageDocument";
|
||||
import ViewDocument from "./ViewDocument";
|
||||
import DocumentViewerModal from "./DocumentViewerModal";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
|
||||
// Context
|
||||
export const DocumentContext = createContext();
|
||||
export const useDocumentContext = () => {
|
||||
const context = useContext(DocumentContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDocumentContext must be used within an DocumentProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const getDocuementsStatus = (status) => {
|
||||
switch (status) {
|
||||
case true:
|
||||
return (
|
||||
<span className="badge rounded-pill bg-label-success">Verified</span>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<span className="badge rounded-pill bg-label-danger">Rejected</span>
|
||||
);
|
||||
case null:
|
||||
default:
|
||||
return (
|
||||
<span className="badge rounded-pill bg-label-warning"> Pending</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
const Documents = ({ Document_Entity, Entity }) => {
|
||||
const [isSelf, setIsSelf] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [filters, setFilter] = useState();
|
||||
const [isRefetching, setIsRefetching] = useState(false);
|
||||
const [refetchFn, setRefetchFn] = useState(null);
|
||||
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
||||
const { employeeId } = useParams();
|
||||
const [OpenDocument, setOpenDocument] = useState(false);
|
||||
const [ManageDoc, setManageDoc] = useState({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
});
|
||||
const [viewDoc, setViewDoc] = useState({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
});
|
||||
const { profile } = useProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.employeeInfo?.id) {
|
||||
setIsSelf(profile.employeeInfo.id === employeeId);
|
||||
}
|
||||
}, [profile?.employeeInfo?.id, employeeId]);
|
||||
const canUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT);
|
||||
|
||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(DocumentFilterSchema),
|
||||
defaultValues: DocumentFilterDefaultValues,
|
||||
});
|
||||
|
||||
const { reset } = methods;
|
||||
|
||||
const clearFilter = () => {
|
||||
setFilter(DocumentFilterDefaultValues);
|
||||
reset();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowTrigger(true);
|
||||
setOffcanvasContent(
|
||||
"Document Filters",
|
||||
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
|
||||
);
|
||||
|
||||
return () => {
|
||||
setShowTrigger(false);
|
||||
setOffcanvasContent("", null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const contextValues = {
|
||||
ManageDoc,
|
||||
setManageDoc,
|
||||
viewDoc,
|
||||
setViewDoc,
|
||||
setOpenDocument,
|
||||
OpenDocument,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (Document_Entity) {
|
||||
setDocumentEntity(Document_Entity);
|
||||
}
|
||||
}, [Document_Entity]);
|
||||
return (
|
||||
<DocumentContext.Provider value={contextValues}>
|
||||
<div className="mt-5">
|
||||
<div className="card d-flex p-2">
|
||||
<div className="row align-items-center">
|
||||
{/* Search */}
|
||||
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
||||
<div className="d-flex">
|
||||
{" "}
|
||||
<input
|
||||
type="search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search Document"
|
||||
/>
|
||||
</div>
|
||||
<label className="switch switch-sm mx-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="switch-input"
|
||||
checked={isActive}
|
||||
onChange={(e) => setIsActive(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-toggle-slider">
|
||||
<span className="switch-on"></span>
|
||||
<span className="switch-off"></span>
|
||||
</span>
|
||||
<span className="switch-label">
|
||||
{isActive ? "Active" : "In-Active"}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="col-6 col-md-6 col-lg-8 text-end">
|
||||
{/* <span
|
||||
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
|
||||
disabled={isRefetching}
|
||||
onClick={() => {
|
||||
setSearchText("");
|
||||
setFilter(DocumentFilterDefaultValues);
|
||||
refetchFn && refetchFn();
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
<i
|
||||
className={`bx bx-refresh ms-1 ${
|
||||
isRefetching ? "bx-spin" : ""
|
||||
}`}
|
||||
></i>
|
||||
</span> */}
|
||||
|
||||
{(isSelf || canUploadDocument) && (
|
||||
<button
|
||||
type="button"
|
||||
title="Add New Document"
|
||||
className="p-1 bg-primary rounded-circle cursor-pointer"
|
||||
onClick={() =>
|
||||
setManageDoc({
|
||||
document: null,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-plus fs-4 text-white"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DocumentsList
|
||||
Document_Entity={DocumentEntity}
|
||||
Entity={Entity}
|
||||
filters={filters}
|
||||
searchText={searchText}
|
||||
setIsRefetching={setIsRefetching}
|
||||
setRefetchFn={setRefetchFn}
|
||||
isActive={isActive}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ManageDoc.isOpen && (
|
||||
<GlobalModel
|
||||
isOpen={ManageDoc.isOpen}
|
||||
closeModal={() =>
|
||||
setManageDoc({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ManageDocument
|
||||
closeModal={() =>
|
||||
setManageDoc({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
})
|
||||
}
|
||||
Document_Entity={DocumentEntity}
|
||||
Entity={Entity}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{viewDoc.isOpen && (
|
||||
<GlobalModel
|
||||
size="lg"
|
||||
isOpen={viewDoc.isOpen}
|
||||
closeModal={() =>
|
||||
setViewDoc({
|
||||
document: null,
|
||||
isOpen: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ViewDocument />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{OpenDocument && (
|
||||
<GlobalModel
|
||||
isOpen={OpenDocument}
|
||||
closeModal={() => setOpenDocument(false)}
|
||||
>
|
||||
<DocumentViewerModal />
|
||||
</GlobalModel>
|
||||
)}
|
||||
</div>
|
||||
</DocumentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Documents;
|
263
src/components/Documents/DocumentsList.jsx
Normal file
263
src/components/Documents/DocumentsList.jsx
Normal file
@ -0,0 +1,263 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
useActiveInActiveDocument,
|
||||
useDocumentListByEntityId,
|
||||
} from "../../hooks/useDocument";
|
||||
import {
|
||||
DELETE_DOCUMENT,
|
||||
ITEMS_PER_PAGE,
|
||||
MODIFY_DOCUMENT,
|
||||
} from "../../utils/constants";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useDebounce } from "../../utils/appUtils";
|
||||
import { DocumentTableSkeleton } from "./DocumentSkeleton";
|
||||
import { getDocuementsStatus, useDocumentContext } from "./Documents";
|
||||
import Pagination from "../common/Pagination";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { isPending } from "@reduxjs/toolkit";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { useParams } from "react-router-dom";
|
||||
import showToast from "../../services/toastService";
|
||||
|
||||
const DocumentsList = ({
|
||||
Document_Entity,
|
||||
Entity,
|
||||
filters,
|
||||
searchText,
|
||||
setIsRefetching,
|
||||
setRefetchFn,
|
||||
isActive,
|
||||
}) => {
|
||||
const { employeeId } = useParams();
|
||||
const [isSelf, setIsSelf] = useState(false);
|
||||
const { profile } = useProfile();
|
||||
const canDeleteDocument = useHasUserPermission(DELETE_DOCUMENT);
|
||||
const canModifyDocument = useHasUserPermission(MODIFY_DOCUMENT);
|
||||
useEffect(() => {
|
||||
if (profile?.employeeInfo?.id && employeeId) {
|
||||
setIsSelf(String(profile.employeeInfo.id) === String(employeeId));
|
||||
}
|
||||
}, [profile?.employeeInfo?.id, employeeId]);
|
||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const [restoringIds, setRestoringIds] = useState([]);
|
||||
const debouncedSearch = useDebounce(searchText, 500);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const { data, isError, isLoading, error, refetch, isFetching } =
|
||||
useDocumentListByEntityId(
|
||||
Document_Entity,
|
||||
Entity,
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
filters,
|
||||
debouncedSearch,
|
||||
isActive
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRefetchFn(() => refetch);
|
||||
}, [setRefetchFn, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRefetching(isFetching);
|
||||
}, [isFetching, setIsRefetching]);
|
||||
|
||||
const { setManageDoc, setViewDoc } = useDocumentContext();
|
||||
const { mutate: ActiveInActive, isPending } = useActiveInActiveDocument();
|
||||
|
||||
const paginate = (page) => {
|
||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
const noData = !isLoading && !isError && data?.data.length === 0;
|
||||
const isSearchEmpty = noData && !!debouncedSearch;
|
||||
const isFilterEmpty = noData && !!filters && Object.keys(filters).length > 0;
|
||||
const isInitialEmpty = noData && !debouncedSearch && !isFilterEmpty;
|
||||
|
||||
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
||||
if (isError)
|
||||
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
||||
if (isInitialEmpty) return <div>No documents found yet.</div>;
|
||||
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
|
||||
if (isFilterEmpty) return <div>No documents match your filter.</div>;
|
||||
|
||||
const handleDelete = () => {
|
||||
ActiveInActive(
|
||||
{ documentId: deletingId, isActive: !isActive },
|
||||
{
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleRestore = (docId) => {
|
||||
setRestoringIds((prev) => [...prev, docId]);
|
||||
|
||||
ActiveInActive(
|
||||
{ documentId: docId, isActive: true },
|
||||
{
|
||||
onSettled: () => {
|
||||
setRestoringIds((prev) => prev.filter((id) => id !== docId));
|
||||
refetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentColumns = [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
getValue: (e) => e.name || "N/A",
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
key: "documentType",
|
||||
label: "Document Type",
|
||||
getValue: (e) => e.documentType?.name || "N/A",
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
key: "uploadedBy",
|
||||
label: "Uploaded By",
|
||||
align: "text-start",
|
||||
customRender: (e) => (
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
classAvatar="m-0"
|
||||
firstName={e.uploadedBy?.firstName}
|
||||
lastName={e.uploadedBy?.lastName}
|
||||
/>
|
||||
<span className="text-truncate ms-1">
|
||||
{`${e.uploadedBy?.firstName ?? ""} ${
|
||||
e.uploadedBy?.lastName ?? ""
|
||||
}`.trim() || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
getValue: (e) =>
|
||||
`${e.uploadedBy?.firstName ?? ""} ${
|
||||
e.uploadedBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
},
|
||||
{
|
||||
key: "uploadedAt",
|
||||
label: "Uploaded on",
|
||||
getValue: (e) => formatUTCToLocalTime(e.uploadedAt),
|
||||
align: "text-center",
|
||||
isAlwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: "Status",
|
||||
label: "Status",
|
||||
getValue: (e) => getDocuementsStatus(e.isVerified),
|
||||
align: "text-center",
|
||||
isAlwaysVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Document"
|
||||
message="Are you sure you want to delete this document?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={!!isPending}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table border-top dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="shadow-sm">
|
||||
{DocumentColumns.map((col) => (
|
||||
<th key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="sticky-action-column bg-white text-center">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-start">
|
||||
{data?.data?.map((doc) => {
|
||||
const isRestoring = restoringIds.includes(doc.id);
|
||||
|
||||
return (
|
||||
<tr key={doc.id}>
|
||||
{DocumentColumns.map((col) => (
|
||||
<td key={col.key} className={`sorting ${col.align}`}>
|
||||
{col.customRender
|
||||
? col.customRender(doc)
|
||||
: col.getValue(doc)}
|
||||
</td>
|
||||
))}
|
||||
<td className="text-center">
|
||||
{doc.isActive ? (
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
<i
|
||||
className="bx bx-show text-primary cursor-pointer"
|
||||
onClick={() =>
|
||||
setViewDoc({ document: doc.id, isOpen: true })
|
||||
}
|
||||
></i>
|
||||
|
||||
{(isSelf || canModifyDocument) && (
|
||||
<i
|
||||
className="bx bx-edit text-secondary cursor-pointer"
|
||||
onClick={() =>
|
||||
setManageDoc({ document: doc.id, isOpen: true })
|
||||
}
|
||||
></i>
|
||||
)}
|
||||
|
||||
{(isSelf || canDeleteDocument) && (
|
||||
<i
|
||||
className="bx bx-trash text-danger cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setDeletingId(doc.id);
|
||||
}}
|
||||
></i>
|
||||
)}
|
||||
</div>
|
||||
) : isRestoring ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className="bx bx-recycle me-1 text-primary cursor-pointer"
|
||||
onClick={() => handleRestore(doc.id)}
|
||||
></i>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsList;
|
428
src/components/Documents/ManageDocument.jsx
Normal file
428
src/components/Documents/ManageDocument.jsx
Normal file
@ -0,0 +1,428 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { defaultDocumentValues, DocumentPayloadSchema } from "./DocumentSchema";
|
||||
import Label from "../common/Label";
|
||||
import {
|
||||
useDocumentCategories,
|
||||
useDocumentTypes,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
import TagInput from "../common/TagInput";
|
||||
import {
|
||||
useDocumentDetails,
|
||||
useDocumentTags,
|
||||
useUpdateDocument,
|
||||
useUploadDocument,
|
||||
} from "../../hooks/useDocument";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useDocumentContext } from "./Documents";
|
||||
import { isPending } from "@reduxjs/toolkit";
|
||||
|
||||
const toBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (err) => reject(err);
|
||||
});
|
||||
|
||||
const MergedTagsWithExistenStatus = (formTags = [], originalTags = []) => {
|
||||
const tagMap = new Map();
|
||||
|
||||
const safeFormTags = Array.isArray(formTags) ? formTags : [];
|
||||
const safeOriginalTags = Array.isArray(originalTags) ? originalTags : [];
|
||||
|
||||
safeOriginalTags.forEach(tag => {
|
||||
if (tag?.name) {
|
||||
tagMap.set(tag.name, { ...tag, isActive: tag.isActive ?? true });
|
||||
}
|
||||
});
|
||||
|
||||
safeFormTags.forEach(tag => {
|
||||
if (tag?.name) {
|
||||
tagMap.set(tag.name, { ...tag, isActive: true });
|
||||
}
|
||||
});
|
||||
|
||||
safeOriginalTags.forEach(tag => {
|
||||
if (tag?.name && !safeFormTags.some(t => t.name === tag.name)) {
|
||||
tagMap.set(tag.name, { ...tag, isActive: false });
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(tagMap.values());
|
||||
};
|
||||
|
||||
|
||||
const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
||||
const { ManageDoc } = useDocumentContext();
|
||||
const isUpdateForm = Boolean(ManageDoc?.document);
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||||
const [schema, setSchema] = useState(() =>
|
||||
DocumentPayloadSchema({ isUpdateForm })
|
||||
);
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultDocumentValues,
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
const { mutate: UploadDocument, isPending: isUploading } = useUploadDocument(
|
||||
() => {
|
||||
showToast("Document Uploaded Successfully", "success");
|
||||
closeModal();
|
||||
}
|
||||
);
|
||||
const { mutate: UpdateDocument, isPending: isUpdating } = useUpdateDocument(
|
||||
() => {
|
||||
showToast("Document Updated Successfully", "success");
|
||||
closeModal();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const onSubmit = (data) => {
|
||||
const normalizeAttachment = (attachment) => {
|
||||
if (!attachment) return null;
|
||||
return {
|
||||
...attachment,
|
||||
fileSize: Math.ceil(attachment.fileSize / 1024),
|
||||
};
|
||||
};
|
||||
|
||||
const payload = {
|
||||
...data,
|
||||
attachment: normalizeAttachment(data.attachment),
|
||||
};
|
||||
|
||||
if (ManageDoc?.document) {
|
||||
const DocumentPayload = {
|
||||
...payload,
|
||||
id: DocData.id,
|
||||
tags: MergedTagsWithExistenStatus(data?.tags, DocData?.tags),
|
||||
};
|
||||
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
|
||||
} else {
|
||||
const DocumentPayload = { ...payload, entityId: Entity };
|
||||
UploadDocument(DocumentPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: DocData,
|
||||
isLoading: isDocLoading,
|
||||
isError: isDocError,
|
||||
DocError,
|
||||
} = useDocumentDetails(ManageDoc?.document);
|
||||
const file = watch("attachment");
|
||||
|
||||
const documentTypeId = watch("documentTypeId");
|
||||
|
||||
// This hooks calling api base Entity(Employee) and Category
|
||||
const { DocumentCategories, isLoading } =
|
||||
useDocumentCategories(Document_Entity);
|
||||
|
||||
const categoryId = watch("documentCategoryId");
|
||||
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
|
||||
categoryId || null
|
||||
);
|
||||
const {data:DocumentTags} = useDocumentTags()
|
||||
|
||||
// Update schema whenever document type changes
|
||||
useEffect(() => {
|
||||
if (!documentTypeId) return;
|
||||
|
||||
const type = DocumentTypes?.find(
|
||||
(t) => String(t.id) === String(documentTypeId)
|
||||
);
|
||||
if (!type) return;
|
||||
setSelectedType(type)
|
||||
const newSchema = DocumentPayloadSchema({
|
||||
isMandatory: type.isMandatory ?? false,
|
||||
regexExpression: type.regexExpression ?? null,
|
||||
allowedContentType: type.allowedContentType ?? [
|
||||
"application/pdf",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
],
|
||||
maxSizeAllowedInMB: type.maxSizeAllowedInMB ?? 25,
|
||||
isUpdateForm,
|
||||
});
|
||||
|
||||
setSchema(() => newSchema);
|
||||
|
||||
methods.reset(methods.getValues(), { keepValues: true });
|
||||
methods.formState.errors; // triggers revalidation
|
||||
}, [documentTypeId, DocumentTypes, isUpdateForm]);
|
||||
|
||||
// File Upload
|
||||
const onFileChange = async (e) => {
|
||||
const uploaded = e.target.files[0];
|
||||
if (!uploaded) return;
|
||||
|
||||
const base64Data = await toBase64(uploaded);
|
||||
|
||||
const parsedFile = {
|
||||
fileName: uploaded.name,
|
||||
base64Data,
|
||||
contentType: uploaded.type,
|
||||
fileSize: uploaded.size,
|
||||
description: "",
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
setValue("attachment", parsedFile, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = () => {
|
||||
setValue("attachment", null, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
// build dynamic file accept string
|
||||
const fileAccept =
|
||||
selectedType?.allowedContentType
|
||||
?.split(",")
|
||||
.map((t) =>
|
||||
t === "application/pdf"
|
||||
? ".pdf"
|
||||
: t === "image/jpeg"
|
||||
? ".jpg,.jpeg"
|
||||
: t === "image/png"
|
||||
? ".png"
|
||||
: ""
|
||||
)
|
||||
.join(",") || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (DocData) {
|
||||
reset({
|
||||
...defaultDocumentValues,
|
||||
name: DocData?.name ?? "",
|
||||
documentCategoryId: DocData?.documentType?.documentCategory?.id
|
||||
? String(DocData.documentType.documentCategory.id)
|
||||
: "",
|
||||
documentTypeId: DocData?.documentType?.id
|
||||
? String(DocData.documentType.id)
|
||||
: "",
|
||||
documentId: DocData?.documentId ?? "",
|
||||
description: DocData?.description ?? "",
|
||||
attachment: DocData?.attachment ?? null,
|
||||
tags: DocData?.tags ?? [],
|
||||
});
|
||||
}
|
||||
}, [DocData, reset]);
|
||||
|
||||
if (isDocLoading) return <div>Loading...</div>;
|
||||
if (isDocError) return <div>{DocError.message}</div>;
|
||||
|
||||
const isPending = isUploading || isUpdating;
|
||||
return (
|
||||
<div className="p-2">
|
||||
<p className="fw-bold fs-6">Upload New Document</p>
|
||||
<FormProvider key={documentTypeId} {...methods}>
|
||||
<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 */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentCategoryId">Document Category</Label>
|
||||
<select
|
||||
{...register("documentCategoryId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
{isLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
)}
|
||||
{!isLoading && <option value="">Select Category</option>}
|
||||
{DocumentCategories?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentCategoryId && (
|
||||
<div className="danger-text">
|
||||
{errors.documentCategoryId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
{categoryId && (
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentTypeId">Document Type</Label>
|
||||
<select
|
||||
{...register("documentTypeId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
{isTypeLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
)}
|
||||
{DocumentTypes?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentTypeId && (
|
||||
<div className="danger-text">
|
||||
{errors.documentTypeId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Document ID */}
|
||||
<div className="mb-2">
|
||||
<Label
|
||||
htmlFor="documentId"
|
||||
required={selectedType?.isMandatory ?? false}
|
||||
>
|
||||
Document ID
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("documentId")}
|
||||
/>
|
||||
{errors.documentId && (
|
||||
<div className="danger-text">{errors.documentId.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upload */}
|
||||
<div className="row my-2">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="attachment" required>Upload Document</Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("attachment").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"></i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">
|
||||
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
|
||||
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
|
||||
</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="attachment"
|
||||
accept={selectedType?.allowedContentType}
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = ""; // reset input
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.attachment && (
|
||||
<small className="danger-text">
|
||||
{errors.attachment.message
|
||||
? errors.attachment.message
|
||||
: errors.attachment.fileName?.message ||
|
||||
errors.attachment.base64Data?.message ||
|
||||
errors.attachment.contentType?.message ||
|
||||
errors.attachment.fileSize?.message}
|
||||
</small>
|
||||
)}
|
||||
|
||||
{file?.base64Data && (
|
||||
<div className="d-flex justify-content-between text-start p-1 mt-2">
|
||||
<div>
|
||||
<span className="mb-0 text-secondary small d-block">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small d-block">
|
||||
{(file.fileSize / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
||||
onClick={removeFile}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<TagInput name="tags" label="Tags" placeholder="Tags.." options={DocumentTags} />
|
||||
{errors.tags && (
|
||||
<small className="danger-text">{errors.tags.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="2"
|
||||
className="form-control"
|
||||
{...register("description")}
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<div className="danger-text">{errors.description.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="d-flex justify-content-end gap-3 mt-4">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-label-secondary btn-sm"
|
||||
disabled={isPending}
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : " Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDocument;
|
47
src/components/Documents/VersionListSkeleton.jsx
Normal file
47
src/components/Documents/VersionListSkeleton.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
|
||||
<div
|
||||
className={`skeleton mb-1 ${className}`}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
|
||||
const VersionListSkeleton = ({ items = 5 }) => {
|
||||
return (
|
||||
<div className="list-group mx-0">
|
||||
{[...Array(items)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="list-group-item py-2 border border-bottom border-top-0 border-start-0 border-end-0"
|
||||
>
|
||||
{/* Top row: document name + version/status */}
|
||||
<div className="d-flex w-100 justify-content-between">
|
||||
<SkeletonLine width="40%" height={16} />
|
||||
<div className="d-flex gap-2">
|
||||
<SkeletonLine width="60px" height={14} />
|
||||
<SkeletonLine width="80px" height={14} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload by row */}
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
<SkeletonLine width="24px" height="24px" className="rounded-circle" />
|
||||
<SkeletonLine width="120px" height={14} />
|
||||
</div>
|
||||
|
||||
{/* Updated at row */}
|
||||
<div className="d-flex gap-2 mt-2">
|
||||
<SkeletonLine width="150px" height={14} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionListSkeleton;
|
175
src/components/Documents/ViewDocument.jsx
Normal file
175
src/components/Documents/ViewDocument.jsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
useDocumentDetails,
|
||||
useDocumentVersionList,
|
||||
useVerifyDocument,
|
||||
} from "../../hooks/useDocument";
|
||||
import { getDocuementsStatus, useDocumentContext } from "./Documents";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../common/Avatar";
|
||||
import {
|
||||
DOWNLOAD_DOCUMENT,
|
||||
ITEMS_PER_PAGE,
|
||||
VERIFY_DOCUMENT,
|
||||
} from "../../utils/constants";
|
||||
import DocumentDetailsSkeleton from "./DocumentDetailsSkeleton ";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import DocumentVersionList from "./DocumentVersionList";
|
||||
|
||||
const ViewDocument = () => {
|
||||
const { viewDoc, setOpenDocument } = useDocumentContext();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showVersions, setShowVersions] = useState(false);
|
||||
const canVerifyDocument = useHasUserPermission(VERIFY_DOCUMENT);
|
||||
|
||||
// Document Details
|
||||
const { data, isLoading, isError, error } = useDocumentDetails(
|
||||
viewDoc?.document
|
||||
);
|
||||
|
||||
// Document Versions (fetch only if toggle is ON)
|
||||
const {
|
||||
data: versionList,
|
||||
isLoading: versionLoading,
|
||||
} = useDocumentVersionList(
|
||||
showVersions ? data?.parentAttachmentId : null,
|
||||
ITEMS_PER_PAGE - 10,
|
||||
currentPage
|
||||
);
|
||||
|
||||
const paginate = (page) => {
|
||||
if (page >= 1 && page <= (versionList?.totalPages ?? 1)) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
// Verify / Reject
|
||||
const { mutate: VerifyDoc, isPending } = useVerifyDocument();
|
||||
const VerifyDocument = () => {
|
||||
VerifyDoc({ documentId: viewDoc?.document, isVerify: true });
|
||||
};
|
||||
const RejectDocument = () => {
|
||||
VerifyDoc({ documentId: viewDoc?.document, isVerify: false });
|
||||
};
|
||||
|
||||
if (isLoading) return <DocumentDetailsSkeleton />;
|
||||
if (isError)
|
||||
return (
|
||||
<div>
|
||||
<p>{error?.response?.data?.message || error?.message}</p>
|
||||
<p className="danger-text">{error?.response?.status}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<p className="fw-bold fs-5 mb-3 border-bottom pb-2">Document Details</p>
|
||||
|
||||
{/* Document Info Rows */}
|
||||
<div className="row mb-2 text-start">
|
||||
<div className="col-12 col-md-6 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Category:
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
{data.documentType?.documentCategory?.name || "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Type:
|
||||
</span>
|
||||
<span className="text-muted">{data.documentType?.name || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-2 text-start">
|
||||
<div className="col-12 col-md-6 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Document Name:
|
||||
</span>
|
||||
<span className="text-muted">{data.name || "-"}</span>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Document ID:
|
||||
</span>
|
||||
<span className="text-muted">{data.documentId || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-2 text-start">
|
||||
<div className="col-12 col-md-6 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Uploaded At:
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
{formatUTCToLocalTime(data.uploadedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="row mb-2 text-start">
|
||||
<div className="col-12 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Tags:
|
||||
</span>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{data.tags?.length > 0 ? (
|
||||
data.tags.map((t, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="badge rounded-pill bg-label-secondary"
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-3 text-start">
|
||||
<div className="col-12 d-flex">
|
||||
<span className="fw-semibold me-2" style={{ minWidth: "140px" }}>
|
||||
Description:
|
||||
</span>
|
||||
<span className="text-muted">{data.description || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle for Versions */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<p className="m-0 fw-semibold fs-6">Documents:</p>
|
||||
<div className="form-check form-switch">
|
||||
<label className="form-check-label" htmlFor="showVersionsSwitch">
|
||||
Show Versions
|
||||
</label>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id="showVersionsSwitch"
|
||||
checked={showVersions}
|
||||
onChange={(e) => setShowVersions(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentVersionList
|
||||
versionLoding={versionLoading}
|
||||
versionList={versionList}
|
||||
isPending={isPending}
|
||||
setOpenDocument={setOpenDocument}
|
||||
VerifyDocument={VerifyDocument}
|
||||
RejectDocument={RejectDocument}
|
||||
showVersions={showVersions}
|
||||
latestDoc={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewDocument;
|
@ -25,7 +25,7 @@ error,
|
||||
<div className="card-body">
|
||||
<div className="my-0 text-start">
|
||||
<DateRangePicker
|
||||
DateDifference="30"
|
||||
DateDifference="7"
|
||||
onRangeChange={setDateRange}
|
||||
endDateMode="today"
|
||||
/>
|
||||
|
@ -128,7 +128,7 @@ const EmpAttendance = ({ employee }) => {
|
||||
>
|
||||
<div className="col-md-4 my-0 ">
|
||||
<DateRangePicker
|
||||
DateDifference="30"
|
||||
DateDifference="7"
|
||||
onRangeChange={setDateRange}
|
||||
endDateMode="today"
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { useChangePassword } from "../../components/Context/ChangePasswordContext";
|
||||
import GlobalModel from "../common/GlobalModel";
|
||||
import ManageEmployee from "./ManageEmployee";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
|
||||
const EmpBanner = ({ profile, loggedInUser }) => {
|
||||
const { openChangePassword } = useChangePassword();
|
||||
@ -77,7 +78,7 @@ const EmpBanner = ({ profile, loggedInUser }) => {
|
||||
{" "}
|
||||
Joined on{" "}
|
||||
{profile?.joiningDate ? (
|
||||
new Date(profile.joiningDate).toLocaleDateString()
|
||||
formatUTCToLocalTime(profile.joiningDate)
|
||||
) : (
|
||||
<em>NA</em>
|
||||
)}
|
||||
|
@ -9,7 +9,6 @@ const EmpDashboard = ({ profile }) => {
|
||||
refetch,
|
||||
} = useProjectsAllocationByEmployee(profile?.id);
|
||||
|
||||
console.log(projectList);
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
||||
import DocumentPage from "../../pages/Documents/DocumentPage";
|
||||
import Documents from "../Documents/Documents";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
|
||||
|
||||
const EmpDocuments = ({ profile, loggedInUser }) => {
|
||||
const {employeeId} = useParams()
|
||||
return (
|
||||
<>
|
||||
<ComingSoonPage/>
|
||||
<Documents Document_Entity={DOCUMENTS_ENTITIES.EmployeeEntity} Entity={employeeId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -142,13 +142,14 @@ const EmpOverview = ({ profile }) => {
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="d-flex align-items-start">
|
||||
<span className="d-flex">
|
||||
<div className="d-flex align-items-start">
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-map bx-xs me-2 mt-1"></i>
|
||||
<span>Address</span>
|
||||
<span className="text-nowrap">Address</span>
|
||||
</span>
|
||||
|
||||
<span style={{ marginLeft: "86px" }}>:</span>
|
||||
<span className="ms-5">
|
||||
<span className="ms-5 text-start" >
|
||||
{profile?.currentAddress || <em>NA</em>}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -4,4 +4,4 @@ const EmployeeList = () => {
|
||||
return <div>EmployeeList</div>;
|
||||
};
|
||||
|
||||
export default EmployeeList;
|
||||
export default EmployeeList;
|
@ -1,12 +1,31 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { VIEW_DOCUMENT } from "../../utils/constants";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { useParams } from "react-router-dom";
|
||||
const EmployeeNav = ({ onPillClick, activePill }) => {
|
||||
const { employeeId } = useParams();
|
||||
const [isAbleToViewDocuments, setIsAbleToViewDocuments] = useState(false);
|
||||
|
||||
const canViewDocuments = useHasUserPermission(VIEW_DOCUMENT);
|
||||
const { profile } = useProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.employeeInfo?.id) {
|
||||
setIsAbleToViewDocuments(profile.employeeInfo.id === employeeId);
|
||||
}
|
||||
}, [profile?.employeeInfo?.id, employeeId]);
|
||||
|
||||
const tabs = [
|
||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||
{ key: "attendance", icon: "bx bx-group", label: "Attendances" },
|
||||
{ key: "documents", icon: "bx bx-user", label: "Documents" },
|
||||
(isAbleToViewDocuments || canViewDocuments) && {
|
||||
key: "documents",
|
||||
icon: "bx bx-file",
|
||||
label: "Documents",
|
||||
},
|
||||
{ key: "activities", icon: "bx bx-grid-alt", label: "Activities" },
|
||||
];
|
||||
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<div className="col-md-12">
|
||||
<div className="nav-align-top">
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
} from "../../slices/apiDataManager";
|
||||
import { clearApiCacheKey } from "../../slices/apiCacheSlice";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
|
||||
const mobileNumberRegex = /^[0-9]\d{9}$/;
|
||||
|
||||
@ -220,10 +222,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
|
||||
<div className="text-center"><p className="fs-6 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p> </div>
|
||||
<div className="text-center"><p className="fs-5 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p> </div>
|
||||
<div className="row mb-3">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">First Name</div>
|
||||
<Label className="form-text text-start" required>First Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
@ -272,7 +274,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">Last Name</div>
|
||||
<Label className="form-text text-start" required>Last Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("lastName", {
|
||||
@ -319,7 +321,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-text text-start">Phone Number</div>
|
||||
<Label className="form-text text-start" required>Phone Number</Label>
|
||||
<input
|
||||
type="text"
|
||||
keyboardType="numeric"
|
||||
@ -343,7 +345,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
<div className="row mb-3"></div>
|
||||
<div className="row mb-3">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">Gender</div>
|
||||
<Label className="form-text text-start" required>Gender</Label>
|
||||
|
||||
<div className="input-group">
|
||||
<select
|
||||
@ -370,41 +372,43 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">Birth Date</div>
|
||||
<Label className="form-text text-start" required>
|
||||
Birth Date
|
||||
</Label>
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="date"
|
||||
{...register("birthDate")}
|
||||
id="birthDate"
|
||||
<DatePicker
|
||||
name="birthDate"
|
||||
control={control} // from useForm()
|
||||
placeholder="DD-MM-YYYY"
|
||||
maxDate={new Date()} // restrict future dates (e.g., birth date must be today or past)
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.birthDate && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
||||
{errors.birthDate.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">Joining Date</div>
|
||||
<Label className="form-text text-start" required>
|
||||
Joining Date
|
||||
</Label>
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="date"
|
||||
{...register("joiningDate")}
|
||||
id="joiningDate"
|
||||
<DatePicker
|
||||
name="joiningDate"
|
||||
control={control} // from useForm()
|
||||
placeholder="DD-MM-YYYY"
|
||||
// For joining date, we don’t block future dates
|
||||
className="w-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.joiningDate && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
||||
{errors.joiningDate.message}
|
||||
</div>
|
||||
)}
|
||||
@ -412,7 +416,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-text text-start">Current Address</div>
|
||||
<Label className="form-text text-start" required>Current Address</Label>
|
||||
|
||||
<textarea
|
||||
id="currentAddress"
|
||||
@ -444,9 +448,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-text text-start">
|
||||
<Label className="form-text text-start" required>
|
||||
Permanent Address
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<textarea
|
||||
id="permanentAddress"
|
||||
@ -484,7 +488,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">Official Designation</div>
|
||||
<Label className="form-text text-start" required>Official Designation</Label>
|
||||
<div className="input-group">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
@ -514,9 +518,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">
|
||||
<Label className="form-text text-start" required>
|
||||
Emergency Contact Person
|
||||
</div>
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("emergencyContactPerson")}
|
||||
@ -535,9 +539,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-text text-start">
|
||||
<Label className="form-text text-start" required>
|
||||
Emergency Phone Number
|
||||
</div>
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("emergencyPhoneNumber")}
|
||||
@ -606,8 +610,16 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row justify-content-start">
|
||||
<div className="row text-end">
|
||||
<div className="col-sm-12">
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
disabled={isPending}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
type="submit"
|
||||
@ -621,14 +633,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||
: "Create"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="manage employee"
|
||||
type="reset"
|
||||
className="btn btn-sm btn-secondary ms-2"
|
||||
disabled={isPending}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -114,7 +114,7 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="text-start mb-3">
|
||||
<p className="lead">Select Roles</p>
|
||||
<h5 className="lead">Select Roles :</h5>
|
||||
</div>
|
||||
|
||||
{isLoadingData ? (
|
||||
@ -148,18 +148,18 @@ const ManageRole = ( {employeeId, onClosed} ) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mt-3">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={isLoading}>
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<div className="text-end mt-1">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isLoading}>
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -71,7 +71,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||
closePanel();
|
||||
};
|
||||
|
||||
// ✅ Close popup when navigating to another component
|
||||
// Close popup when navigating to another component
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
closePanel();
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
APPROVE_EXPENSE,
|
||||
EXPENSE_DRAFT,
|
||||
EXPENSE_REJECTEDBY,
|
||||
ITEMS_PER_PAGE,
|
||||
} from "../../utils/constants";
|
||||
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
|
||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||
@ -22,12 +23,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
const IsExpenseEditable = useHasUserPermission();
|
||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 20;
|
||||
const debouncedSearch = useDebounce(searchText, 500);
|
||||
|
||||
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
||||
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
||||
pageSize,
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
filters,
|
||||
debouncedSearch
|
||||
@ -110,8 +110,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
label: "Submitted By",
|
||||
align: "text-start",
|
||||
getValue: (e) =>
|
||||
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""}`.trim() ||
|
||||
"N/A",
|
||||
`${e.createdBy?.firstName ?? ""} ${
|
||||
e.createdBy?.lastName ?? ""
|
||||
}`.trim() || "N/A",
|
||||
customRender: (e) => (
|
||||
<div className="d-flex align-items-center">
|
||||
<Avatar
|
||||
@ -185,26 +186,16 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||
return (
|
||||
<>
|
||||
{IsDeleteModalOpen && (
|
||||
<div
|
||||
className={`modal fade show`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: "block",
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type="delete"
|
||||
header="Delete Expense"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={isPending}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModalOpen}
|
||||
type="delete"
|
||||
header="Delete Expense"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleDelete}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
loading={isPending}
|
||||
paramData={deletingId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="card px-0 px-sm-4">
|
||||
|
@ -27,6 +27,7 @@ import ExpenseSkeleton from "./ExpenseSkeleton";
|
||||
import moment from "moment";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import ErrorPage from "../../pages/ErrorPage";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
const {
|
||||
@ -213,9 +214,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
{expenseToEdit ? "Update Expense " : "Create New Expense"}
|
||||
</h5>
|
||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Select Project</label>
|
||||
<Label className="form-label" required>Select Project</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
@ -237,9 +238,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="expensesTypeId" className="form-label">
|
||||
<Label htmlFor="expensesTypeId" className="form-label" required>
|
||||
Expense Type
|
||||
</label>
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="expensesTypeId"
|
||||
@ -266,11 +267,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="paymentModeId" className="form-label">
|
||||
<Label htmlFor="paymentModeId" className="form-label" required>
|
||||
Payment Mode
|
||||
</label>
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
@ -297,9 +298,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="paidById" className="form-label ">
|
||||
<Label htmlFor="paidById" className="form-label" required>
|
||||
Paid By
|
||||
</label>
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
@ -325,11 +326,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="transactionDate" className="form-label ">
|
||||
<Label htmlFor="transactionDate" className="form-label" required>
|
||||
Transaction Date
|
||||
</label>
|
||||
</Label>
|
||||
<DatePicker name="transactionDate" control={control} />
|
||||
|
||||
{errors.transactionDate && (
|
||||
@ -340,9 +341,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="amount" className="form-label ">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
@ -358,11 +359,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="supplerName" className="form-label ">
|
||||
<Label htmlFor="supplerName" className="form-label" required>
|
||||
Supplier Name/Transporter Name/Other
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="supplerName"
|
||||
@ -377,9 +378,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="location" className="form-label ">
|
||||
<Label htmlFor="location" className="form-label" required>
|
||||
Location
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
@ -391,7 +392,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="statusId" className="form-label ">
|
||||
Transaction ID
|
||||
@ -428,7 +429,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
|
||||
{ExpenseType?.noOfPersonsRequired && (
|
||||
<div className="col-md-6 mt-2">
|
||||
<div className="col-md-6 mt-2 text-start">
|
||||
<label className="form-label ">No. of Persons</label>
|
||||
<input
|
||||
type="number"
|
||||
@ -446,9 +447,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<label htmlFor="description" className="form-label ">Description</label>
|
||||
<Label htmlFor="description" className="form-label" required>Description</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
@ -463,16 +464,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<label className="form-label ">Upload Bill </label>
|
||||
<Label className="form-label" required>Upload Bill </Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("billAttachments").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"></i>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"> </i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
@ -547,8 +548,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-center gap-3">
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
{" "}
|
||||
<button
|
||||
type="reset"
|
||||
disabled={isPending || createPending}
|
||||
onClick={handleClose}
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
@ -560,14 +569,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
disabled={isPending || createPending}
|
||||
onClick={handleClose}
|
||||
className="btn btn-secondary btn-sm mt-3"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -28,6 +28,7 @@ import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||
import { z } from "zod";
|
||||
import moment from "moment";
|
||||
import ExpenseStatusLogs from "./ExpenseStatusLogs";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const ViewExpense = ({ ExpenseId }) => {
|
||||
const { data, isLoading, isError, error, isFetching } = useExpense(ExpenseId);
|
||||
@ -418,7 +419,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
|
||||
(IsRejectedExpense && isCreatedBy)) && (
|
||||
<>
|
||||
<label className="form-label me-2 mb-0">Comment:</label>
|
||||
<Label className="form-label me-2 mb-0" required>Comment</Label>
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
{...register("comment")}
|
||||
@ -434,13 +435,13 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
|
||||
{nextStatusWithPermission?.length > 0 &&
|
||||
(!IsRejectedExpense || isCreatedBy) && (
|
||||
<div className="text-center flex-wrap gap-2 my-2">
|
||||
<div className="text-end flex-wrap gap-2 my-2 mt-3">
|
||||
{nextStatusWithPermission.map((status, index) => (
|
||||
<button
|
||||
key={status.id || index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClickedStatusId(status.id);
|
||||
setClickedStatusId(status.id);
|
||||
setValue("statusId", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
|
@ -51,13 +51,13 @@ const AboutProject = () => {
|
||||
)}
|
||||
{projects_Details && (
|
||||
<>
|
||||
<div className="card mb-6">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header text-start">
|
||||
<h6 className="card-action-title mb-0 ps-1">
|
||||
<h5 className="card-action-title mb-0 ps-1">
|
||||
{" "}
|
||||
<i className="fa fa-building rounded-circle text-primary"></i>
|
||||
<span className="ms-2">Project Profile</span>
|
||||
</h6>
|
||||
<span className="ms-2 fw-bold">Project Profile</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-unstyled my-3 ps-0">
|
||||
|
@ -12,6 +12,7 @@ import showToast from "../../services/toastService";
|
||||
import { useProjectDetails } from "../../hooks/useProjects";
|
||||
import eventBus from "../../services/eventBus";
|
||||
import { useCreateTask } from "../../hooks/useTasks";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
const maxPlanned =
|
||||
@ -261,8 +262,8 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
<div className="dropdown position-relative d-inline-block">
|
||||
<a
|
||||
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
|
||||
? "text-secondary"
|
||||
: "text-primary"
|
||||
? "text-secondary"
|
||||
: "text-primary"
|
||||
}`}
|
||||
data-bs-toggle="dropdown"
|
||||
role="button"
|
||||
@ -427,7 +428,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
) : (
|
||||
<div className="col-12">
|
||||
<p className="text-center">
|
||||
No employees found for the selected role.
|
||||
No employees found for the selected role.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -502,8 +503,11 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
<div className="col-md text-start mx-0 px-0">
|
||||
<div className="form-check form-check-inline mt-2 px-1 mb-2 text-start">
|
||||
<label className="text-dark d-flex align-items-center flex-wrap form-text">
|
||||
<span>Target for Today</span>
|
||||
<span style={{ marginLeft: "46px" }}>:</span>
|
||||
<Label htmlFor="targetToday" required className="me-1">
|
||||
Target for Today
|
||||
</Label>
|
||||
|
||||
<span style={{ marginLeft: "37px" }}>:</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@ -533,12 +537,16 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label
|
||||
{/* <label
|
||||
className="form-text fs-7 m-1 text-lg text-dark"
|
||||
htmlFor="descriptionTextarea" // Changed htmlFor for better accessibility
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
</label> */}
|
||||
<Label className="form-text fs-7 m-1 text-lg text-dark"
|
||||
htmlFor="descriptionTextarea" required>
|
||||
Description
|
||||
</Label>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
@ -552,14 +560,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
</div>
|
||||
|
||||
{/* Submit and Cancel buttons */}
|
||||
<div className="col-12 d-flex justify-content-center align-items-center gap-sm-6 gap-8 text-center mt-1">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary "
|
||||
disabled={isSubmitting || loading}
|
||||
>
|
||||
{isSubmitting ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
<div className="col-12 d-flex justify-content-end align-items-center gap-2 mt-6">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
@ -570,7 +571,15 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isSubmitting || loading}
|
||||
>
|
||||
{isSubmitting ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import { getCachedData } from "../../../slices/apiDataManager";
|
||||
import showToast from "../../../services/toastService";
|
||||
import { useManageProjectInfra } from "../../../hooks/useProjects";
|
||||
import useSelect from "../../common/useSelect";
|
||||
import Label from "../../common/Label";
|
||||
|
||||
const buildingSchema = z.object({
|
||||
Id: z.string().optional(),
|
||||
@ -100,7 +101,7 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmitHandler)} className="row g-2">
|
||||
<h5 className="text-center mb-2">Manage Buildings </h5>
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Building</label>
|
||||
<select
|
||||
{...register("Id")}
|
||||
@ -121,10 +122,10 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>
|
||||
{watchedId !== "0" ? "Rename Building Name" : "New Building Name"}
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
{...register("name")}
|
||||
type="text"
|
||||
@ -136,8 +137,8 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Description</label>
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>Description</Label>
|
||||
<textarea
|
||||
{...register("description")}
|
||||
rows="5"
|
||||
@ -149,10 +150,21 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<div className="col-12 text-end mt-5">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending
|
||||
@ -162,17 +174,7 @@ const BuildingModel = ({ project, onClose, editingBuilding = null }) => {
|
||||
: "Add Building"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -134,7 +134,7 @@ useEffect(() => {
|
||||
<h5 className="mb-1">Manage Task</h5>
|
||||
</div>
|
||||
|
||||
<div className="row g-2">
|
||||
<div className="row g-2 text-start">
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label">Select Building</label>
|
||||
<input
|
||||
@ -154,7 +154,7 @@ useEffect(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Work Area</label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
@ -163,7 +163,7 @@ useEffect(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Activity</label>
|
||||
<select
|
||||
{...register("activityID")}
|
||||
@ -185,7 +185,7 @@ useEffect(() => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Work Category</label>
|
||||
<select
|
||||
{...register("workCategoryId")}
|
||||
@ -207,7 +207,7 @@ useEffect(() => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-5">
|
||||
<div className="col-5 text-start">
|
||||
<label className="form-label">Planned Work</label>
|
||||
<input
|
||||
{...register("plannedWork", { valueAsNumber: true })}
|
||||
@ -219,7 +219,7 @@ useEffect(() => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-5">
|
||||
<div className="col-5 text-start">
|
||||
<label className="form-label">Completed Work</label>
|
||||
<input
|
||||
{...register("completedWork", { valueAsNumber: true })}
|
||||
@ -232,7 +232,7 @@ useEffect(() => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-2">
|
||||
<div className="col-2 text-start">
|
||||
<label className="form-label">Unit</label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
@ -241,7 +241,7 @@ useEffect(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Comment</label>
|
||||
<textarea {...register("comment")} rows="2" className="form-control" />
|
||||
{errors.comment && (
|
||||
@ -249,22 +249,22 @@ useEffect(() => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-2"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Edit Task"}
|
||||
</button>
|
||||
<div className="col-12 text-end mt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-2"
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Edit Task"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import showToast from "../../../services/toastService";
|
||||
import { useManageProjectInfra } from "../../../hooks/useProjects";
|
||||
import { useSelector } from "react-redux";
|
||||
import Label from "../../common/Label";
|
||||
|
||||
// Schema
|
||||
const floorSchema = z.object({
|
||||
@ -101,8 +102,8 @@ const FloorModel = ({ project, onClose, onSubmit }) => {
|
||||
<div className="text-center mb-1">
|
||||
<h5 className="mb-1">Manage Floor</h5>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Select Building</label>
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>Select Building</Label>
|
||||
<select
|
||||
{...register("buildingId")}
|
||||
className="form-select form-select-sm"
|
||||
@ -126,7 +127,7 @@ const FloorModel = ({ project, onClose, onSubmit }) => {
|
||||
|
||||
{watchBuildingId !== "0" && (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Floor</label>
|
||||
<select
|
||||
{...register("id")}
|
||||
@ -146,10 +147,10 @@ const FloorModel = ({ project, onClose, onSubmit }) => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>
|
||||
{watchId !== "0" ? "Edit Floor Name" : "New Floor Name"}
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
{...register("floorName")}
|
||||
className="form-control form-control-sm"
|
||||
@ -162,10 +163,18 @@ const FloorModel = ({ project, onClose, onSubmit }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<div className="col-12 text-end mt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
disabled={isPending}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending
|
||||
@ -174,14 +183,7 @@ const FloorModel = ({ project, onClose, onSubmit }) => {
|
||||
? "Edit Floor"
|
||||
: "Add Floor"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
disabled={isPending}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from "../../../hooks/masterHook/useMaster";
|
||||
import { useManageTask } from "../../../hooks/useProjects";
|
||||
import showToast from "../../../services/toastService";
|
||||
import Label from "../../common/Label";
|
||||
|
||||
const taskSchema = z.object({
|
||||
buildingID: z.string().min(1, "Building is required"),
|
||||
@ -106,8 +107,8 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
<div className="text-center mb-1">
|
||||
<h5 className="mb-1">Manage Task</h5>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<label className="form-label">Select Building</label>
|
||||
<div className="col-6 text-start">
|
||||
<Label className="form-label" required>Select Building</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("buildingID")}
|
||||
@ -128,8 +129,8 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
</div>
|
||||
|
||||
{selectedBuilding && (
|
||||
<div className="col-6">
|
||||
<label className="form-label">Select Floor</label>
|
||||
<div className="col-6 text-start">
|
||||
<Label className="form-label" required>Select Floor</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("floorId")}
|
||||
@ -150,8 +151,8 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
)}
|
||||
|
||||
{selectedFloor && (
|
||||
<div className="col-12">
|
||||
<label className="form-label">Select Work Area</label>
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>Select Work Area</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("workAreaId")}
|
||||
@ -172,8 +173,8 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
)}
|
||||
|
||||
{selectedWorkArea && (
|
||||
<div className="col-12">
|
||||
<label className="form-label">Select Activity</label>
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>Select Activity</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("activityID")}
|
||||
@ -192,7 +193,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
)}
|
||||
|
||||
{selectedWorkArea && (
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Work Category</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
@ -212,8 +213,8 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
|
||||
{selectedActivity && selectedCategory && (
|
||||
<>
|
||||
<div className="col-5">
|
||||
<label className="form-label">Planned Work</label>
|
||||
<div className="col-5 text-start">
|
||||
<Label className="form-label" required>Planned Work</Label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
@ -223,7 +224,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
<p className="danger-text">{errors.plannedWork.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-5">
|
||||
<div className="col-5 text-start">
|
||||
<label className="form-label">Completed Work</label>
|
||||
<input
|
||||
type="number"
|
||||
@ -234,7 +235,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
<p className="danger-text">{errors.completedWork.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-2">
|
||||
<div className="col-2 text-start">
|
||||
<label className="form-label">Unit</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -247,7 +248,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
)}
|
||||
|
||||
{selectedActivity && selectedCategory && (
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Comment</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
@ -260,21 +261,21 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Please Wait..." : "Add Task"}
|
||||
</button>
|
||||
<div className="col-12 text-end mt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Please Wait..." : "Add Task"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -5,13 +5,14 @@ import { z } from "zod";
|
||||
import showToast from "../../../services/toastService";
|
||||
import { useManageProjectInfra } from "../../../hooks/useProjects";
|
||||
import { useSelector } from "react-redux";
|
||||
import Label from "../../common/Label";
|
||||
|
||||
const workAreaSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
buildingId: z.string().refine((val) => val !== "0", {
|
||||
message: "Building is required",
|
||||
}),
|
||||
floorId: z.string().refine((val)=>val !== "0",{message:"Floor is required"}),
|
||||
message: "Building is required",
|
||||
}),
|
||||
floorId: z.string().refine((val) => val !== "0", { message: "Floor is required" }),
|
||||
areaName: z.string().min(3, "Work Area Name must be at least 3 characters"),
|
||||
});
|
||||
|
||||
@ -25,7 +26,7 @@ const defaultModel = {
|
||||
const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
||||
const [selectedBuilding, setSelectedBuilding] = useState(null);
|
||||
const [selectedFloor, setSelectedFloor] = useState(null);
|
||||
const selectedProject = useSelector((store)=>store.localVariables.projectId)
|
||||
const selectedProject = useSelector((store) => store.localVariables.projectId)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -84,8 +85,7 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
||||
reset(defaultModel);
|
||||
}, []);
|
||||
|
||||
const onSubmitForm = ( data ) =>
|
||||
{
|
||||
const onSubmitForm = (data) => {
|
||||
const payload = {
|
||||
id: data.id === "0" ? null : data.id,
|
||||
areaName: data.areaName,
|
||||
@ -108,51 +108,51 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
||||
<div className="text-center mb-1">
|
||||
<h5 className="mb-1">Manage Work Area</h5>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Select Building</label>
|
||||
<div className="col-12 col-sm-6 text-start">
|
||||
<Label className="form-label" required>Select Building</Label>
|
||||
<select
|
||||
{...register("buildingId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
<option value="0">Select Building</option>
|
||||
{project?.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.buildingName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.buildingId && (
|
||||
<p className="danger-text">{errors.buildingId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{watchBuildingId !== "0" && (
|
||||
<div className="col-12 col-sm-6 text-start">
|
||||
<Label className="form-label" required>Select Floor</Label>
|
||||
<select
|
||||
{...register("buildingId")}
|
||||
{...register("floorId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
<option value="0">Select Building</option>
|
||||
{project?.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.buildingName}
|
||||
<option value="0">
|
||||
{selectedBuilding?.floor?.length > 0
|
||||
? "NO Floor Found"
|
||||
: "Select Floor"}
|
||||
</option>
|
||||
|
||||
{selectedBuilding?.floors?.map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.floorName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.buildingId && (
|
||||
<p className="danger-text">{errors.buildingId.message}</p>
|
||||
{errors.floorId && (
|
||||
<p className="danger-text">{errors.floorId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{watchBuildingId !== "0" && (
|
||||
<div className="col-12 col-sm-6">
|
||||
<label className="form-label">Select Floor</label>
|
||||
<select
|
||||
{...register("floorId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
<option value="0">
|
||||
{selectedBuilding?.floor?.length > 0
|
||||
? "NO Floor Found"
|
||||
: "Select Floor"}
|
||||
</option>
|
||||
|
||||
{selectedBuilding?.floors?.map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.floorName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.floorId && (
|
||||
<p className="danger-text">{errors.floorId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{watchFloorId !== "0" && (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Select Work Area</label>
|
||||
<select
|
||||
{...register("id")}
|
||||
@ -169,12 +169,12 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">
|
||||
<div className="col-12 text-start">
|
||||
<Label className="form-label" required>
|
||||
{watchWorkAreaId === "0"
|
||||
? "Enter Work Area Name"
|
||||
: "Edit Work Area Name"}
|
||||
</label>
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
@ -187,14 +187,15 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={isPending}>
|
||||
{isPending ? "Please Wait.." : watchWorkAreaId === "0" ? "Add Work Area" : "Update Work Area"}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-label-secondary" disabled={isPending} onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-12 text-end mt-5">
|
||||
<button type="button" className="btn btn-sm btn-label-secondary me-3" disabled={isPending} onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isPending}>
|
||||
{isPending ? "Please Wait.." : watchWorkAreaId === "0" ? "Add Work Area" : "Update Work Area"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -32,8 +32,8 @@ const WorkItem = ({
|
||||
forWorkArea,
|
||||
deleteHandleTask,
|
||||
}) => {
|
||||
const projectId = useSelector((store)=>store.localVariables.projectId)
|
||||
const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
|
||||
|
||||
const [itemName, setItemName] = useState("");
|
||||
const [NewWorkItem, setNewWorkItem] = useState();
|
||||
@ -135,25 +135,15 @@ const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
|
||||
)}
|
||||
|
||||
{showModal2 && (
|
||||
<div
|
||||
className={`modal fade ${showModal2 ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: showModal2 ? "block" : "none",
|
||||
backgroundColor: showModal2 ? "rgba(0,0,0,0.5)" : "transparent",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Activity"}
|
||||
message={"Are you sure you want delete?"}
|
||||
onSubmit={handleSubmit}
|
||||
onClose={closeModalDelete}
|
||||
loading={loadingDelete}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={showModal2}
|
||||
type="delete"
|
||||
header="Delete Activity"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={closeModalDelete}
|
||||
loading={isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
<tr key={NewWorkItem?.workItemId}>
|
||||
@ -240,9 +230,7 @@ const isTaskPlanning = /^\/activities\/task$/.test(location.pathname);
|
||||
</td>
|
||||
|
||||
{(ManageInfra ||
|
||||
(
|
||||
ManageAndAssignTak &&
|
||||
PlannedWork !== CompletedWork)) && (
|
||||
(ManageAndAssignTak && PlannedWork !== CompletedWork)) && (
|
||||
<td className="text-end align-items-middle border-top">
|
||||
{/* Desktop (md and up): inline icons */}
|
||||
<div className="d-none d-md-flex justify-content-end gap-1 px-2">
|
||||
|
@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
|
||||
const currentDate = new Date().toLocaleDateString('en-CA');
|
||||
const formatDate = (date) => {
|
||||
@ -14,14 +16,14 @@ const formatDate = (date) => {
|
||||
}
|
||||
return d.toLocaleDateString('en-CA');
|
||||
};
|
||||
const ManageProjectInfo = ({ project, handleSubmitForm, onClose,isPending }) => {
|
||||
const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) => {
|
||||
const [CurrentProject, setCurrentProject] = useState();
|
||||
const [addressLength, setAddressLength] = useState(0);
|
||||
const maxAddressLength = 500;
|
||||
|
||||
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
||||
const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
||||
const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const projectSchema = z
|
||||
.object({
|
||||
...(project?.id ? { id: z.string().optional() } : {}),
|
||||
@ -93,15 +95,15 @@ const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
reset(
|
||||
project
|
||||
? {
|
||||
id: project?.id || "",
|
||||
name: project?.name || "",
|
||||
shortName: project?.shortName || "",
|
||||
contactPerson: project?.contactPerson || "",
|
||||
projectAddress: project?.projectAddress || "",
|
||||
startDate: formatDate(project?.startDate) || "",
|
||||
endDate: formatDate(project?.endDate) || "",
|
||||
projectStatusId: String(project?.projectStatus?.id) || "00000000-0000-0000-0000-000000000000",
|
||||
}
|
||||
id: project?.id || "",
|
||||
name: project?.name || "",
|
||||
shortName: project?.shortName || "",
|
||||
contactPerson: project?.contactPerson || "",
|
||||
projectAddress: project?.projectAddress || "",
|
||||
startDate: formatDate(project?.startDate) || "",
|
||||
endDate: formatDate(project?.endDate) || "",
|
||||
projectStatusId: String(project?.projectStatus?.id) || "00000000-0000-0000-0000-000000000000",
|
||||
}
|
||||
: {}
|
||||
);
|
||||
setAddressLength(project?.projectAddress?.length || 0);
|
||||
@ -115,9 +117,8 @@ const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
*/
|
||||
|
||||
const onSubmitForm = ( updatedProject ) =>
|
||||
{
|
||||
|
||||
const onSubmitForm = (updatedProject) => {
|
||||
|
||||
handleSubmitForm(updatedProject);
|
||||
};
|
||||
|
||||
@ -137,197 +138,202 @@ const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
return (
|
||||
|
||||
<div className="p-sm-2 p-2">
|
||||
|
||||
<div className="text-center mb-2">
|
||||
<h5 className="mb-2">
|
||||
{project?.id ? "Edit Project" : "Create Project"}
|
||||
</h5>
|
||||
</div>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="name">
|
||||
Project Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Project Name"
|
||||
{...register("name")}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.name.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="shortName">
|
||||
Short Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shortName"
|
||||
name="shortName"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Short Name"
|
||||
{...register("shortName")}
|
||||
/>
|
||||
{errors.shortName && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.shortName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="contactPerson">
|
||||
Contact Person
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="contactPerson"
|
||||
name="contactPerson"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Contact Person"
|
||||
maxLength={50}
|
||||
{...register("contactPerson")}
|
||||
/>
|
||||
{errors.contactPerson && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.contactPerson.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-sm-2 p-2">
|
||||
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="startDate">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="date"
|
||||
name="startDate"
|
||||
{...register("startDate")}
|
||||
id="startDate"
|
||||
/>
|
||||
{errors.startDate && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.startDate.message}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center mb-2">
|
||||
<h5 className="mb-2">
|
||||
{project?.id ? "Edit Project" : "Create Project"}
|
||||
</h5>
|
||||
</div>
|
||||
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<Label htmlFor="name" required>
|
||||
Project Name
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Project Name"
|
||||
{...register("name")}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.name.message}
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="endDate">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="date"
|
||||
name="endDate"
|
||||
{...register("endDate")}
|
||||
id="endDate"
|
||||
/>
|
||||
{errors.endDate && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.endDate.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="modalEditUserStatus"
|
||||
name="modalEditUserStatus"
|
||||
className="select2 form-select form-select-sm"
|
||||
aria-label="Default select example"
|
||||
{...register("projectStatusId", {
|
||||
required: "Status is required",
|
||||
valueAsNumber: false,
|
||||
})}
|
||||
>
|
||||
{/* <option disabled>Status</option>
|
||||
<option value="b74da4c2-d07e-46f2-9919-e75e49b12731">Active</option> */}
|
||||
<option value={ACTIVE_STATUS_ID}>Active</option>
|
||||
<option value="603e994b-a27f-4e5d-a251-f3d69b0498ba">On Hold</option>
|
||||
|
||||
<option value="cdad86aa-8a56-4ff4-b633-9c629057dfef">In Progress</option>
|
||||
<option value="ef1c356e-0fe0-42df-a5d3-8daee355492d">Inactive</option>
|
||||
|
||||
<option value="33deaef9-9af1-4f2a-b443-681ea0d04f81">Completed</option>
|
||||
</select>
|
||||
{errors.projectStatusId && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.projectStatusId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="projectAddress">
|
||||
Project Address
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<textarea
|
||||
id="projectAddress"
|
||||
name="projectAddress"
|
||||
className="form-control"
|
||||
maxLength={maxAddressLength}
|
||||
{...register("projectAddress")}
|
||||
onChange={(e) => {
|
||||
setAddressLength(e.target.value.length);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-end" style={{ fontSize: "12px" }}>
|
||||
{maxAddressLength - addressLength} characters left
|
||||
</div>
|
||||
{errors.projectAddress && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.projectAddress.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3" disabled={isPending}>
|
||||
{isPending ? "Please Wait..." : project?.id ? "Update" : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={handleCancel}
|
||||
aria-label="Close"
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="shortName">
|
||||
Short Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shortName"
|
||||
name="shortName"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Short Name"
|
||||
{...register("shortName")}
|
||||
/>
|
||||
{errors.shortName && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.shortName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<Label htmlFor="contactPerson" required>
|
||||
Contact Person
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
id="contactPerson"
|
||||
name="contactPerson"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Contact Person"
|
||||
maxLength={50}
|
||||
{...register("contactPerson")}
|
||||
/>
|
||||
{errors.contactPerson && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.contactPerson.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="startDate">
|
||||
Start Date
|
||||
</label>
|
||||
|
||||
<DatePicker
|
||||
name="startDate"
|
||||
control={control}
|
||||
placeholder="DD-MM-YYYY"
|
||||
maxDate={new Date()} // optional: restrict future dates
|
||||
className="w-100"
|
||||
/>
|
||||
|
||||
{errors.startDate && (
|
||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
||||
{errors.startDate.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="endDate">
|
||||
End Date
|
||||
</label>
|
||||
|
||||
<DatePicker
|
||||
name="endDate"
|
||||
control={control}
|
||||
placeholder="DD-MM-YYYY"
|
||||
minDate={getValues("startDate")} // optional: restrict future dates
|
||||
className="w-100"
|
||||
/>
|
||||
|
||||
{errors.endDate && (
|
||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
||||
{errors.endDate.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="modalEditUserStatus"
|
||||
name="modalEditUserStatus"
|
||||
className="select2 form-select form-select-sm"
|
||||
aria-label="Default select example"
|
||||
{...register("projectStatusId", {
|
||||
required: "Status is required",
|
||||
valueAsNumber: false,
|
||||
})}
|
||||
>
|
||||
{/* <option disabled>Status</option>
|
||||
<option value="b74da4c2-d07e-46f2-9919-e75e49b12731">Active</option> */}
|
||||
<option value={ACTIVE_STATUS_ID}>Active</option>
|
||||
<option value="603e994b-a27f-4e5d-a251-f3d69b0498ba">On Hold</option>
|
||||
|
||||
<option value="cdad86aa-8a56-4ff4-b633-9c629057dfef">In Progress</option>
|
||||
<option value="ef1c356e-0fe0-42df-a5d3-8daee355492d">Inactive</option>
|
||||
|
||||
<option value="33deaef9-9af1-4f2a-b443-681ea0d04f81">Completed</option>
|
||||
</select>
|
||||
{errors.projectStatusId && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.projectStatusId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<Label htmlFor="projectAddress" required>
|
||||
Project Address
|
||||
</Label>
|
||||
<div className="input-group">
|
||||
<textarea
|
||||
id="projectAddress"
|
||||
name="projectAddress"
|
||||
className="form-control"
|
||||
maxLength={maxAddressLength}
|
||||
{...register("projectAddress")}
|
||||
onChange={(e) => {
|
||||
setAddressLength(e.target.value.length);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-end" style={{ fontSize: "12px" }}>
|
||||
{maxAddressLength - addressLength} characters left
|
||||
</div>
|
||||
{errors.projectAddress && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.projectAddress.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 text-end mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-label-secondary btn-sm me-2"
|
||||
onClick={handleCancel}
|
||||
aria-label="Close"
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : project?.id ? "Update" : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
19
src/components/Project/MapUser.css
Normal file
19
src/components/Project/MapUser.css
Normal file
@ -0,0 +1,19 @@
|
||||
/* For Webkit browsers (Chrome, Edge, Safari) */
|
||||
.modal-dialog-scrollable::-webkit-scrollbar {
|
||||
width: 6px; /* smaller width */
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.3); /* scrollbar color */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.modal-dialog-scrollable {
|
||||
scrollbar-width: thin; /* shrinks scrollbar */
|
||||
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
|
||||
}
|
@ -4,6 +4,7 @@ import { useAllEmployees } from "../../hooks/useEmployees";
|
||||
import useSearch from "../../hooks/useSearch";
|
||||
import AssignEmployeeTable from "./AssignEmployeeTable";
|
||||
import showToast from "../../services/toastService";
|
||||
import "./MapUser.css";
|
||||
|
||||
const MapUsers = ({
|
||||
projectId,
|
||||
@ -118,22 +119,22 @@ const MapUsers = ({
|
||||
<div className="modal-content">
|
||||
<div className="modal-header text-center">
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<p className="m-0 fw-semibold fs-5">Assign Employee</p>
|
||||
|
||||
<div className="px-4 mt-4 col-md-4 text-start">
|
||||
{(filteredData.length > 0 ||
|
||||
allocationEmployeesData.length > 0) && (
|
||||
<div className="input-group input-group-sm mb-2">
|
||||
<input
|
||||
type="search"
|
||||
className="form-control"
|
||||
placeholder="Search employees..."
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="input-group input-group-sm mb-2">
|
||||
<input
|
||||
type="search"
|
||||
className="form-control"
|
||||
placeholder="Search employees..."
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mb-0 small text-muted fw-semibold">Select Employee</p>
|
||||
</div>
|
||||
@ -183,24 +184,24 @@ const MapUsers = ({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{(filteredData.length > 0 ||
|
||||
allocationEmployeesData.length > 0) && (
|
||||
<button className="btn btn-sm btn-success" onClick={handleSubmit}>
|
||||
{assignedLoading ? "Please Wait..." : "Assign to Project"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="modal-footer mt-5 d-flex justify-content-end gap-0">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
{(filteredData.length > 0 || allocationEmployeesData.length > 0) && (
|
||||
<button className="btn btn-sm btn-primary" onClick={handleSubmit}>
|
||||
{assignedLoading ? "Please Wait..." : "Assign to Project"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
14
src/components/Project/ProjectDocuments.jsx
Normal file
14
src/components/Project/ProjectDocuments.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import Documents from "../Documents/Documents";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
|
||||
const ProjectDocuments = () => {
|
||||
const selectedProject = useSelectedProject()
|
||||
return (
|
||||
<>
|
||||
<Documents Document_Entity={DOCUMENTS_ENTITIES.ProjectEntity} Entity={selectedProject} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDocuments;
|
@ -93,7 +93,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="link-button link-button-sm m-1 "
|
||||
className="link-button link-button-sm m-1 btn-primary"
|
||||
onClick={()=>setshowModalBuilding(true)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
@ -101,7 +101,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-button m-1"
|
||||
className="link-button m-1 btn-primary"
|
||||
onClick={()=>setshowModalFloor(true)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
@ -109,7 +109,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-button m-1"
|
||||
className="link-button m-1 btn-primary"
|
||||
onClick={() => setshowModalWorkArea(true)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
@ -117,7 +117,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-button m-1"
|
||||
className="link-button m-1 btn-primary"
|
||||
onClick={()=>setshowModalTask(true)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
|
@ -1,84 +1,57 @@
|
||||
import React from "react";
|
||||
import { hasUserPermission } from "../../utils/authUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER, DIRECTORY_USER, VIEW_PROJECT_INFRA } from "../../utils/constants";
|
||||
import {
|
||||
DIRECTORY_ADMIN,
|
||||
DIRECTORY_MANAGER,
|
||||
DIRECTORY_USER,
|
||||
VIEW_PROJECT_INFRA,
|
||||
} from "../../utils/constants";
|
||||
|
||||
const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
const HasViewInfraStructure = useHasUserPermission( VIEW_PROJECT_INFRA );
|
||||
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER)
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER)
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
||||
|
||||
const ProjectTab = [
|
||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||
{ key: "teams", icon: "bx bx-group", label: "Teams" },
|
||||
{
|
||||
key: "infra",
|
||||
icon: "bx bx-grid-alt",
|
||||
label: "Infrastructure",
|
||||
hidden: !HasViewInfraStructure,
|
||||
},
|
||||
{
|
||||
key: "directory",
|
||||
icon: "bx bxs-contact",
|
||||
label: "Directory",
|
||||
hidden: !(DirAdmin || DireManager || DirUser),
|
||||
},
|
||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents" },
|
||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting" },
|
||||
];
|
||||
return (
|
||||
<div className="nav-align-top">
|
||||
<ul className="nav nav-tabs ">
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "profile" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("profile");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-user bx-sm me-1_5"></i> <span className="d-none d-md-inline">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "teams" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("teams");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-group bx-sm me-1_5"></i><span className="d-none d-md-inline" > Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className={`nav-item ${!HasViewInfraStructure && "d-none"} `}>
|
||||
<a
|
||||
className={`nav-link ${activePill === "infra" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("infra");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-grid-alt bx-sm me-1_5"></i> <span className="d-none d-md-inline">Infrastructure</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
{(DirAdmin || DireManager || DirUser) && (
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "directory" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent page reload
|
||||
onPillClick("directory");
|
||||
}}
|
||||
>
|
||||
<i className='bx bxs-contact bx-sm me-1_5'></i> <span className="d-none d-md-inline">Directory</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${
|
||||
activePill === "imagegallary" ? "active" : ""
|
||||
} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent page reload
|
||||
onPillClick("imagegallary");
|
||||
}}
|
||||
>
|
||||
<i className='bx bxs-cog bx-sm me-1_5'></i> <span className="d-none d-md-inline">project Setup</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<ul className="nav nav-tabs">
|
||||
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
|
||||
<li key={tab.key} className="nav-item cursor-pointer">
|
||||
<a
|
||||
|
||||
className={`nav-link ${
|
||||
activePill === tab.key ? "active cursor-pointer" : ""
|
||||
} fs-6`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick(tab.key);
|
||||
}}
|
||||
>
|
||||
<i className={`${tab.icon} bx-sm me-1_5`}></i>
|
||||
<span className="d-none d-md-inline ">{tab.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
@ -167,11 +167,11 @@ const ProjectOverview = ({ project }) => {
|
||||
return (
|
||||
<div className="card" style={{ minHeight: "490px" }}>
|
||||
<div className="card-header text-start">
|
||||
<h6 className="card-action-title mb-0">
|
||||
<h5 className="card-action-title mb-0">
|
||||
{" "}
|
||||
<i className="fa fa-line-chart rounded-circle text-primary"></i>
|
||||
<span className="ms-2">Project Statistics</span>
|
||||
</h6>
|
||||
<span className="ms-2 fw-bold">Project Statistics</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-unstyled m-0 p-0">
|
||||
|
181
src/components/Project/ProjectPermission.jsx
Normal file
181
src/components/Project/ProjectPermission.jsx
Normal file
@ -0,0 +1,181 @@
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
useProjectLevelEmployeePermission,
|
||||
useProjectLevelModules,
|
||||
useUpdateProjectLevelEmployeePermission,
|
||||
} from "../../hooks/useProjects";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import showToast from "../../services/toastService";
|
||||
|
||||
export const ProjectPermissionSchema = z.object({
|
||||
employeeId: z.string().min(1, "Employee is required"),
|
||||
selectedPermissions: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const ProjectPermission = () => {
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const { data: ProjectModules = [] } = useProjectLevelModules();
|
||||
const { employees = [], loading } = useEmployeesByProject(selectedProject);
|
||||
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ProjectPermissionSchema),
|
||||
defaultValues: {
|
||||
employeeId: "",
|
||||
selectedPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
const selectedEmployee = watch("employeeId");
|
||||
|
||||
const { data: selectedEmpPermissions } = useProjectLevelEmployeePermission(
|
||||
selectedEmployee || "",
|
||||
selectedProject
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!employees.length) return;
|
||||
|
||||
const enabledPerms =
|
||||
selectedEmpPermissions?.permissions
|
||||
?.filter((perm) => perm.isEnabled)
|
||||
?.map((perm) => perm.id) || [];
|
||||
|
||||
reset({
|
||||
employeeId: selectedEmployee || employees[0]?.id || "",
|
||||
selectedPermissions: enabledPerms,
|
||||
});
|
||||
}, [selectedEmpPermissions, reset, selectedEmployee, employees]);
|
||||
|
||||
const { mutate: updatePermission, isPending } =
|
||||
useUpdateProjectLevelEmployeePermission();
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
if (!formData.employeeId) {
|
||||
showToast("Please select an employee", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
||||
|
||||
const payloadPermissions =
|
||||
existingPermissions.length > 0
|
||||
? existingPermissions.map((perm) => ({
|
||||
id: perm.id,
|
||||
isEnabled: formData.selectedPermissions?.includes(perm.id) || false,
|
||||
}))
|
||||
: (formData.selectedPermissions || []).map((id) => ({
|
||||
id,
|
||||
isEnabled: true,
|
||||
}));
|
||||
|
||||
if (payloadPermissions.length === 0) {
|
||||
showToast("No permissions selected", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = existingPermissions.some(
|
||||
(perm) =>
|
||||
perm.isEnabled !==
|
||||
(formData.selectedPermissions?.includes(perm.id) || false)
|
||||
);
|
||||
|
||||
if (!hasChanges && existingPermissions.length > 0) {
|
||||
showToast("No changes detected", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
employeeId: formData.employeeId,
|
||||
projectId: selectedProject,
|
||||
permission: payloadPermissions,
|
||||
};
|
||||
|
||||
updatePermission(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row px-2 py-1">
|
||||
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Employee Dropdown */}
|
||||
<div className="d-flex align-items-end gap-2">
|
||||
<div className="text-start">
|
||||
<label className="form-label">Select Employee</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("employeeId")}
|
||||
disabled={isPending}
|
||||
>
|
||||
{loading ? (
|
||||
<option value="">Loading...</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">-- Select --</option>
|
||||
{employees.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>
|
||||
{emp.firstName} {emp.lastName}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
{errors.employeeId && (
|
||||
<div className="text-danger small">
|
||||
{errors.employeeId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-sm btn-primary" disabled={isPending || loading}>
|
||||
{isPending ? "Please Wait..." : "Update Permission"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
{ProjectModules.map((feature) => (
|
||||
<div key={feature.id} className="row my-2 px-3 ">
|
||||
<div className="col-12 text-start fw-semibold mb-2">
|
||||
{feature.name}
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
{feature.featurePermissions?.map((perm) => (
|
||||
<div
|
||||
className="col-12 col-sm-6 col-md-4 mb-2"
|
||||
key={perm.id}
|
||||
>
|
||||
<label
|
||||
className="form-check-label d-flex align-items-center"
|
||||
htmlFor={perm.id}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input me-2"
|
||||
id={perm.id}
|
||||
value={perm.id}
|
||||
{...register("selectedPermissions")}
|
||||
/>
|
||||
{perm.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-2" />
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectPermission;
|
74
src/components/Project/ProjectSetting.jsx
Normal file
74
src/components/Project/ProjectSetting.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
||||
import ProjectPermission from "./ProjectPermission";
|
||||
|
||||
const ProjectSetting = () => {
|
||||
const [activePill, setActivePill] = useState(() => {
|
||||
return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
||||
});
|
||||
const projectSettingTab = [
|
||||
{ key: "Permissions", label: "Permissions" },
|
||||
{ key: "Notification", label: "Notification" },
|
||||
{ key: "SeparatedLink", label: "Separated link", isButton: true },
|
||||
];
|
||||
const handlePillClick = (pillKey) => {
|
||||
setActivePill(pillKey);
|
||||
localStorage.setItem("lastActiveProjectSettingTab", pillKey);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activePill) {
|
||||
case "Permissions":
|
||||
return <ProjectPermission />;
|
||||
|
||||
case "Notification":
|
||||
return <ComingSoonPage />;
|
||||
|
||||
default:
|
||||
return <ComingSoonPage />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-100">
|
||||
<div className="card py-2 px-5">
|
||||
<div className="col-4">
|
||||
<div className="dropdown text-start">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{activePill || "Select Option"}
|
||||
</button>
|
||||
|
||||
<ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
{projectSettingTab.map((item) =>
|
||||
item.isButton ? (
|
||||
<li key={item.key}>
|
||||
<button className="dropdown-item">{item.label}</button>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item.key}>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => handlePillClick(item.key)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">{renderContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSetting;
|
@ -249,26 +249,15 @@ const Teams = () => {
|
||||
</div>
|
||||
|
||||
{IsDeleteModal && (
|
||||
<div
|
||||
className={`modal fade ${IsDeleteModal ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: IsDeleteModal ? "block" : "none",
|
||||
backgroundColor: IsDeleteModal ? "rgba(0,0,0,0.5)" : "transparent",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Removed Employee"}
|
||||
message={"Are you sure you want delete?"}
|
||||
onSubmit={removeAllocation}
|
||||
onClose={closeDeleteModal}
|
||||
loading={isPending}
|
||||
paramData={deleteEmployee}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={IsDeleteModal}
|
||||
type="delete"
|
||||
header="Removed Employee"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={() => removeAllocation(deleteEmployee)}
|
||||
onClose={closeDeleteModal}
|
||||
loading={isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="card card-action mb-6">
|
||||
|
@ -24,7 +24,7 @@ const ContactInfro = ({ onNext }) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="row g-6">
|
||||
<div className="row g-6 text-start">
|
||||
<div className="col-sm-6">
|
||||
<Label htmlFor="firstName" required>
|
||||
First Name
|
||||
|
@ -56,7 +56,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="row g-2">
|
||||
<div className="row g-2 text-start">
|
||||
<div className="col-sm-6">
|
||||
<Label htmlFor="organizationName" required>
|
||||
Organization Name
|
||||
@ -223,10 +223,10 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between mt-3">
|
||||
<div className="d-flex justify-content-end mt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
className="btn btn-sm btn-secondary me-3"
|
||||
onClick={onPrev}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
@ -233,7 +233,7 @@ const SubScription = ({ onSubmitSubScription, onNext }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex text-center mt-4">
|
||||
<div className="d-flex text-end mt-4">
|
||||
<button
|
||||
onClick={handleSubscriptionSubmit}
|
||||
className="btn btn-sm btn-primary"
|
||||
|
@ -1,70 +1,71 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from "react";
|
||||
|
||||
const ConfirmModal = ({ type, onSubmit, onClose, message, loading ,header, paramData}) => {
|
||||
|
||||
const TypeofIcon = (type) => {
|
||||
switch (type) {
|
||||
case "delete":
|
||||
return <i className='bx bx-x-circle text-danger ' style={{fontSize:"60px"}} ></i>;
|
||||
default:
|
||||
return null;
|
||||
const ConfirmModal = ({
|
||||
type,
|
||||
onSubmit,
|
||||
onClose,
|
||||
message,
|
||||
loading,
|
||||
header,
|
||||
paramData,
|
||||
isOpen = false,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const TypeofIcon = () => {
|
||||
if (type === "delete") {
|
||||
return (
|
||||
<i
|
||||
className="bx bx-x-circle text-danger"
|
||||
style={{ fontSize: "60px" }}
|
||||
></i>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const TypeofModal = (type) => {
|
||||
switch (type) {
|
||||
case "delete":
|
||||
return "sm";
|
||||
case "other":
|
||||
return "md";
|
||||
default:
|
||||
return "sm";
|
||||
}
|
||||
};
|
||||
const modalSize = type === "delete" ? "sm" : "md";
|
||||
|
||||
return (
|
||||
<div className={`modal-dialog modal-${TypeofModal(type)} modal-simple modal-confirm`}>
|
||||
<div className='modal-dialog modal-dialog-centered'>
|
||||
<div
|
||||
className="modal fade show"
|
||||
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className={`modal-dialog modal-${modalSize} modal-dialog-top`}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body py-1 px-2">
|
||||
<div className="row">
|
||||
|
||||
<div className="text-start mb-1">
|
||||
|
||||
<div className=' d-flex justify-content-between mb-4'>
|
||||
{header && < strong className='mb-0 font-weight-bold'>{header }</strong>}
|
||||
<div className="d-flex justify-content-between mb-4 pt-2">
|
||||
{header && <strong className="mb-0">{header}</strong>}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
className="btn-close "
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div className='col-4 col-sm-2'> {TypeofIcon(type)}</div>
|
||||
<div className='col-8 col-sm-10 py-sm-2 py-1 text-sm-start'>
|
||||
<span className='fs-6 text'>{message}</span>
|
||||
<div className='d-flex justify-content-end mt-4'>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-4 col-sm-2">{TypeofIcon()}</div>
|
||||
<div className="col-8 col-sm-10 py-sm-2 py-1 text-sm-start">
|
||||
<span className="fs-6">{message}</span>
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<button
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={()=>onSubmit(paramData)}
|
||||
disabled={loading}
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => onSubmit(paramData)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Please Wait..." : "Yes"}
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-secondary ms-4 btn-sm'
|
||||
className="btn btn-secondary ms-4 btn-sm"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ const DatePicker = ({
|
||||
placeholder = "DD-MM-YYYY",
|
||||
className = "",
|
||||
allowText = false,
|
||||
maxDate=new Date(),
|
||||
maxDate, // removed default new Date()
|
||||
minDate,
|
||||
...rest
|
||||
}) => {
|
||||
@ -25,19 +25,25 @@ const DatePicker = ({
|
||||
if (inputRef.current) {
|
||||
flatpickr(inputRef.current, {
|
||||
dateFormat: "d-m-Y",
|
||||
allowInput: allowText,
|
||||
allowInput: allowText,
|
||||
defaultDate: value
|
||||
? flatpickr.parseDate(value, "Y-m-d")
|
||||
: null,
|
||||
maxDate:maxDate,
|
||||
minDate:new Date(minDate?.split("T")[0]) ?? undefined,
|
||||
onChange: function (selectedDates, dateStr) {
|
||||
onChange(dateStr);
|
||||
maxDate: maxDate ?? undefined, // only applied if passed
|
||||
minDate: minDate ? new Date(minDate.split("T")[0]) : undefined,
|
||||
onChange: function (selectedDates) {
|
||||
if (selectedDates.length > 0) {
|
||||
// store in YYYY-MM-DD
|
||||
const formatted = flatpickr.formatDate(selectedDates[0], "Y-m-d");
|
||||
onChange(formatted);
|
||||
} else {
|
||||
onChange("");
|
||||
}
|
||||
},
|
||||
...rest
|
||||
});
|
||||
}
|
||||
}, [inputRef]);
|
||||
}, [inputRef, value, allowText, maxDate, minDate, rest, onChange]);
|
||||
|
||||
return (
|
||||
<div className={` position-relative ${className}`}>
|
||||
@ -45,8 +51,13 @@ const DatePicker = ({
|
||||
type="text"
|
||||
className="form-control form-control-sm "
|
||||
placeholder={placeholder}
|
||||
defaultValue={
|
||||
value ? flatpickr.formatDate(flatpickr.parseDate(value, "Y-m-d"), "d-m-Y") : ""
|
||||
defaultValue={
|
||||
value
|
||||
? flatpickr.formatDate(
|
||||
flatpickr.parseDate(value, "Y-m-d"),
|
||||
"d-m-Y"
|
||||
)
|
||||
: ""
|
||||
}
|
||||
ref={(el) => {
|
||||
inputRef.current = el;
|
||||
|
@ -84,6 +84,7 @@ export const DateRangePicker1 = ({
|
||||
allowText = false,
|
||||
resetSignal,
|
||||
defaultRange = true,
|
||||
maxDate = null,
|
||||
...rest
|
||||
}) => {
|
||||
const inputRef = useRef(null);
|
||||
@ -117,6 +118,7 @@ export const DateRangePicker1 = ({
|
||||
mode: "range",
|
||||
dateFormat: "d-m-Y",
|
||||
allowInput: allowText,
|
||||
maxDate ,
|
||||
onChange: (selectedDates) => {
|
||||
if (selectedDates.length === 2) {
|
||||
const [start, end] = selectedDates;
|
||||
|
@ -30,7 +30,7 @@ const FilterIcon = ({
|
||||
}, [currentSelectedBuilding, currentSelectedFloors, currentSelectedActivities, selectedProject]);
|
||||
|
||||
const getUniqueFilterValues = (key, overrideBuilding, overrideFloors) => {
|
||||
if (!taskListData) return [];
|
||||
if (!taskListData || taskListData.length === 0) return [];
|
||||
|
||||
let filteredTasks = [...taskListData];
|
||||
|
||||
@ -125,6 +125,11 @@ const FilterIcon = ({
|
||||
const appliedFilterCount =
|
||||
(appliedBuilding ? 1 : 0) + appliedFloors.length + appliedActivities.length;
|
||||
|
||||
// ✅ Hide filter if no task data
|
||||
if (!taskListData || taskListData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown" style={{ marginLeft: "-14px", position: "relative" }}>
|
||||
<a
|
||||
|
@ -110,7 +110,7 @@ const SelectMultiple = ({
|
||||
onChange={() => handleCheckboxChange(valueVal)}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<label className="text-secondary">{labelVal}</label>
|
||||
<label className="fw-semibold">{labelVal}</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -1,116 +1,73 @@
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Label from "./Label";
|
||||
|
||||
const TagInput = ({
|
||||
label = "Tags",
|
||||
name = "tags",
|
||||
placeholder = "Start typing to add... like employee, manager",
|
||||
color = "#e9ecef",
|
||||
options = [],
|
||||
}) => {
|
||||
const [tags, setTags] = useState([]);
|
||||
const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) => {
|
||||
const { setValue, watch } = useFormContext();
|
||||
const tags = watch(name) || [];
|
||||
const [input, setInput] = useState("");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const { setValue, trigger, control } = useFormContext();
|
||||
const watchedTags = useWatch({ control, name });
|
||||
|
||||
const handleAdd = (newTag) => {
|
||||
const tagObj = typeof newTag === "string" ? { name: newTag, isActive: true } : newTag;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
Array.isArray(watchedTags) &&
|
||||
JSON.stringify(tags) !== JSON.stringify(watchedTags)
|
||||
) {
|
||||
setTags(watchedTags);
|
||||
}
|
||||
}, [JSON.stringify(watchedTags)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (input.trim() === "") {
|
||||
setSuggestions([]);
|
||||
} else {
|
||||
const filtered = options?.filter(
|
||||
(opt) =>
|
||||
opt?.name?.toLowerCase()?.includes(input.toLowerCase()) &&
|
||||
!tags?.some((tag) => tag.name === opt.name)
|
||||
);
|
||||
setSuggestions(filtered);
|
||||
if (!tags.some((t) => t.name === tagObj.name)) {
|
||||
setValue(name, [...tags, tagObj], { shouldValidate: true });
|
||||
}
|
||||
}, [input, options, tags]);
|
||||
};
|
||||
|
||||
const addTag = async (tagObj) => {
|
||||
if (!tags.some((tag) => tag.name === tagObj.name)) {
|
||||
const cleanedTag = {
|
||||
id: tagObj.id ?? null,
|
||||
name: tagObj.name,
|
||||
};
|
||||
const newTags = [...tags, cleanedTag];
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
await trigger(name);
|
||||
const handleRemove = (tagName) => {
|
||||
setValue(
|
||||
name,
|
||||
tags.filter((t) => t.name !== tagName),
|
||||
{ shouldValidate: true }
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if ((e.key === "Enter" || e.key === " ") && input.trim()) {
|
||||
e.preventDefault();
|
||||
handleAdd(input.trim());
|
||||
setInput("");
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (indexToRemove) => {
|
||||
const newTags = tags.filter((_, i) => i !== indexToRemove);
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
trigger(name);
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
const val = e.target.value;
|
||||
setInput(val);
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if ((e.key === "Enter" || e.key === " ")&& input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if (e.key === "Backspace" && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
const handleInputKey = (e) => {
|
||||
const key = e.key?.toLowerCase();
|
||||
|
||||
if ((key === "enter" || key === " " || e.code === "Space") && input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
if (val) {
|
||||
setSuggestions(
|
||||
options
|
||||
.filter((opt) => {
|
||||
const label = typeof opt === "string" ? opt : opt.name;
|
||||
return (
|
||||
label.toLowerCase().includes(val.toLowerCase()) &&
|
||||
!tags.some((t) => t.name === label)
|
||||
);
|
||||
})
|
||||
.map((opt) => ({
|
||||
name: typeof opt === "string" ? opt : opt.name,
|
||||
isActive: true,
|
||||
}))
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if ((key === "backspace" || e.code === "Backspace") && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
addTag(suggestion);
|
||||
const handleSuggestionClick = (sugg) => {
|
||||
handleAdd(sugg);
|
||||
setInput("");
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const backgroundColor = color || "#f8f9fa";
|
||||
const iconColor = `var(--bs-${color})`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={name} className="form-label">
|
||||
<Label htmlFor={name} className="form-label" required>
|
||||
{label}
|
||||
</label>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className="form-control form-control-sm p-1"
|
||||
@ -122,17 +79,16 @@ useEffect(() => {
|
||||
key={index}
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
color: iconColor,
|
||||
backgroundColor,
|
||||
backgroundColor: color,
|
||||
padding: "2px 6px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "0.8rem",
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
<i
|
||||
className="bx bx-x bx-xs ms-1"
|
||||
onClick={() => removeTag(index)}
|
||||
onClick={() => handleRemove(tag.name)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</span>
|
||||
@ -141,9 +97,8 @@ useEffect(() => {
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKey}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
border: "none",
|
||||
@ -156,12 +111,13 @@ useEffect(() => {
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<ul
|
||||
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm "
|
||||
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm"
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
boxShadow:"0px 4px 10px rgba(0, 0, 0, 0.2)",borderRadius:"3px",border:"1px solid #ddd"
|
||||
borderRadius: "3px",
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
{suggestions.map((sugg, i) => (
|
||||
@ -169,8 +125,7 @@ useEffect(() => {
|
||||
key={i}
|
||||
className="dropdown-item p-1 hoverBox"
|
||||
onClick={() => handleSuggestionClick(sugg)}
|
||||
style={{cursor: "pointer", fontSize: "0.875rem"}}
|
||||
|
||||
style={{ cursor: "pointer", fontSize: "0.875rem" }}
|
||||
>
|
||||
{sugg.name}
|
||||
</li>
|
||||
@ -181,4 +136,7 @@ useEffect(() => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default TagInput;
|
||||
|
@ -8,6 +8,7 @@ import { clearApiCacheKey } from "../../slices/apiCacheSlice";
|
||||
import { getCachedData, cacheData } from "../../slices/apiDataManager";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useCreateActivity } from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const schema = z.object({
|
||||
activityName: z.string().min(1, { message: "Activity Name is required" }),
|
||||
@ -118,8 +119,8 @@ const CreateActivity = ({ onClose }) => {
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* <h6>Create Activity</h6> */}
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<label className="form-label">Activity</label>
|
||||
<div className="col-6 text-start">
|
||||
<Label className="form-label" required>Activity</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("activityName")}
|
||||
@ -131,8 +132,8 @@ const CreateActivity = ({ onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-6">
|
||||
<label className="form-label">Measurement</label>
|
||||
<div className="col-6 text-start">
|
||||
<Label className="form-label" required>Measurement</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("unitOfMeasurement")}
|
||||
@ -219,17 +220,17 @@ const CreateActivity = ({ onClose }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center mt-3">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
<div className="col-12 text-end mt-3">
|
||||
<button
|
||||
type="button" // ✅ change to button
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useCreateContactCategory } from '../../hooks/masterHook/useMaster';
|
||||
import {useCreateContactCategory} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -64,17 +65,17 @@ const CreateContactCategory = ({ onClose }) => {
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Category Name</label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Category Name</Label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -92,21 +93,19 @@ const CreateContactCategory = ({ onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // clear inputs
|
||||
onClose?.(); // close modal from parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useCreateContactTag } from '../../hooks/masterHook/useMaster';
|
||||
import {useCreateContactTag} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -64,17 +65,17 @@ const CreateContactTag = ({ onClose }) => {
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Tag Name</label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Tag Name</Label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -92,21 +93,19 @@ const CreateContactTag = ({ onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // clear inputs
|
||||
onClose?.(); // close modal from parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useCreateJobRole } from '../../hooks/masterHook/useMaster';
|
||||
import {useCreateJobRole} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -91,16 +92,16 @@ const CreateJobRole = ({ onClose }) => {
|
||||
{/* <div className="col-12 col-md-12">
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Job Role</label>
|
||||
</div> */}
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Role</label>
|
||||
<input type="text"
|
||||
{...register("role")}
|
||||
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Role</Label>
|
||||
<input type="text"
|
||||
{...register("role")}
|
||||
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -118,21 +119,19 @@ const CreateJobRole = ({ onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ change from reset → button
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // optional: clears form
|
||||
onClose?.(); // ✅ close modal via parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -8,6 +8,7 @@ import { MasterRespository } from "../../repositories/MastersRepository";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useCreateApplicationRole } from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const schema = z.object({
|
||||
role: z.string().min(1, { message: "Role is required" }),
|
||||
@ -109,8 +110,8 @@ const CreateRole = ({ modalType, onClose }) => {
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Application Role</label>
|
||||
|
||||
</div> */}
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Role</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Role</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("role")}
|
||||
@ -119,10 +120,10 @@ const CreateRole = ({ modalType, onClose }) => {
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>
|
||||
Description
|
||||
</label>
|
||||
</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -227,18 +228,19 @@ const CreateRole = ({ modalType, onClose }) => {
|
||||
</div>
|
||||
|
||||
{masterFeatures && (
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={onClose}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useCreateWorkCategory } from '../../hooks/masterHook/useMaster';
|
||||
import {useCreateWorkCategory} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -76,21 +77,21 @@ const CreateWorkCategory = ({ onClose }) => {
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* <div className="col-12 col-md-12">
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Work Category</label>
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Create Work Category</label>
|
||||
</div> */}
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Category Name</label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Category Name</Label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -108,20 +109,19 @@ const CreateWorkCategory = ({ onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // clear inputs
|
||||
onClose?.(); // close modal from parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -1,48 +1,46 @@
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useDeleteMasterItem } from "../../hooks/masterHook/useMaster";
|
||||
|
||||
const API_URL = "http://localhost:5000/mastersdata";
|
||||
|
||||
const DeleteMaster = ({ master, onClose }) => {
|
||||
const [loader, setLoader] = useState(false);
|
||||
const { mutate: deleteMaster, isPending } = useDeleteMasterItem();
|
||||
|
||||
const handleDelete = () => {
|
||||
const index = mastersdata[master?.masterType]?.findIndex(
|
||||
(item) => String(item?.id) === String(master?.item?.id)
|
||||
deleteMaster(
|
||||
{ masterType: master?.masterType, item: master?.item },
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
}
|
||||
);
|
||||
if (index !== -1) {
|
||||
mastersdata[master?.masterType].splice(index, 1);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row g-2">
|
||||
<p className="h5">Are your sure , you want delete</p>
|
||||
<div className="col-12 d-flex justify-content-between width-md-6 width-sm-12">
|
||||
<p className="h5">Are you sure you want to delete?</p>
|
||||
<div className="col-12 d-flex justify-content-between">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary me-3"
|
||||
type="button"
|
||||
className="btn btn-danger me-3"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
{loader ? (
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
{isPending ? (
|
||||
<div className="spinner-border text-light" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
"Delete"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
type="button"
|
||||
className="btn btn-label-secondary"
|
||||
onClick={() => {
|
||||
onClose?.(); // properly close modal
|
||||
}}
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,10 +2,11 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {MasterRespository} from "../../repositories/MastersRepository";
|
||||
import { MasterRespository } from "../../repositories/MastersRepository";
|
||||
import showToast from "../../services/toastService";
|
||||
import {getCachedData,cacheData} from "../../slices/apiDataManager";
|
||||
import {useUpdateActivity} from "../../hooks/masterHook/useMaster";
|
||||
import { getCachedData, cacheData } from "../../slices/apiDataManager";
|
||||
import { useUpdateActivity } from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -26,88 +27,88 @@ const schema = z.object({
|
||||
const UpdateActivity = ({ activityData, onClose }) => {
|
||||
const { mutate: updateActivity, isPending: isLoading } = useUpdateActivity(() => onClose?.());
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
reset,
|
||||
setError,
|
||||
clearErrors,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
id: activityData?.id,
|
||||
activityName: activityData?.activityName,
|
||||
unitOfMeasurement: activityData?.unitOfMeasurement,
|
||||
checkList: activityData?.checkLists || [],
|
||||
},
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
reset,
|
||||
setError,
|
||||
clearErrors,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
id: activityData?.id,
|
||||
activityName: activityData?.activityName,
|
||||
unitOfMeasurement: activityData?.unitOfMeasurement,
|
||||
checkList: activityData?.checkLists || [],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields: checkListItems, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "checkList",
|
||||
});
|
||||
const { fields: checkListItems, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "checkList",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (activityData) {
|
||||
reset({
|
||||
id: activityData.id,
|
||||
activityName: activityData.activityName,
|
||||
unitOfMeasurement: activityData.unitOfMeasurement,
|
||||
checkList: activityData.checkLists || [],
|
||||
});
|
||||
}
|
||||
}, [activityData, reset]);
|
||||
useEffect(() => {
|
||||
if (activityData) {
|
||||
reset({
|
||||
id: activityData.id,
|
||||
activityName: activityData.activityName,
|
||||
unitOfMeasurement: activityData.unitOfMeasurement,
|
||||
checkList: activityData.checkLists || [],
|
||||
});
|
||||
}
|
||||
}, [activityData, reset]);
|
||||
|
||||
const addChecklistItem = () => {
|
||||
const values = getValues("checkList");
|
||||
const lastIndex = checkListItems.length - 1;
|
||||
const addChecklistItem = () => {
|
||||
const values = getValues("checkList");
|
||||
const lastIndex = checkListItems.length - 1;
|
||||
|
||||
if (
|
||||
checkListItems.length > 0 &&
|
||||
(!values?.[lastIndex] || values[lastIndex].description.trim() === "")
|
||||
) {
|
||||
setError(`checkList.${lastIndex}.description`, {
|
||||
type: "manual",
|
||||
message: "Please fill this checklist item before adding another.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
checkListItems.length > 0 &&
|
||||
(!values?.[lastIndex] || values[lastIndex].description.trim() === "")
|
||||
) {
|
||||
setError(`checkList.${lastIndex}.description`, {
|
||||
type: "manual",
|
||||
message: "Please fill this checklist item before adding another.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
clearErrors(`checkList.${lastIndex}.description`);
|
||||
append({ id: null, description: "", isMandatory: false });
|
||||
};
|
||||
|
||||
const removeChecklistItem = (index) => {
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const handleChecklistChange = (index, value) => {
|
||||
setValue(`checkList.${index}`, value);
|
||||
};
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const payload = { ...formData, id: activityData.id };
|
||||
updateActivity({ id: activityData.id, payload });
|
||||
clearErrors(`checkList.${lastIndex}.description`);
|
||||
append({ id: null, description: "", isMandatory: false });
|
||||
};
|
||||
// const onSubmit = async(data) => {
|
||||
|
||||
const removeChecklistItem = (index) => {
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const handleChecklistChange = (index, value) => {
|
||||
setValue(`checkList.${index}`, value);
|
||||
};
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const payload = { ...formData, id: activityData.id };
|
||||
updateActivity({ id: activityData.id, payload });
|
||||
};
|
||||
// const onSubmit = async(data) => {
|
||||
// setIsLoading(true);
|
||||
|
||||
|
||||
// const Activity = {...data, id:activityData.id}
|
||||
// try
|
||||
// {
|
||||
// const response = await MasterRespository.updateActivity( activityData?.id, Activity );
|
||||
// const updatedActivity = response.data;
|
||||
// const cachedData = getCachedData("Activity")
|
||||
|
||||
|
||||
// if (cachedData) {
|
||||
// const updatedActivities = cachedData.map((activity) =>
|
||||
// activity.id === updatedActivity.id ? { ...activity, ...updatedActivity } : activity
|
||||
// );
|
||||
|
||||
|
||||
// cacheData( "Activity", updatedActivities );
|
||||
// onClose()
|
||||
// }
|
||||
@ -121,18 +122,18 @@ const onSubmit = (formData) => {
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* <h6>Update Activity</h6> */}
|
||||
<div className="row">
|
||||
{/* Activity Name */}
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Activity Name</label>
|
||||
<div className="col-md-6 text-start">
|
||||
<Label className="form-label" required>Activity Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("activityName")}
|
||||
@ -144,8 +145,8 @@ useEffect(() => {
|
||||
</div>
|
||||
|
||||
{/* Unit of Measurement */}
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Measurement</label>
|
||||
<div className="col-md-6 text-start">
|
||||
<Label className="form-label" required>Measurement</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("unitOfMeasurement")}
|
||||
@ -160,93 +161,94 @@ useEffect(() => {
|
||||
<div className="col-md-12 text-start mt-1">
|
||||
<p className="py-1 my-0">{checkListItems.length > 0 ? "Check List" : "Add Check List"}</p>
|
||||
{checkListItems.length > 0 && (
|
||||
<table className="table mt-1 border-0">
|
||||
<thead className="py-0 my-0 table-border-top-0">
|
||||
<tr className="py-1">
|
||||
<th colSpan={2} className="py-1">
|
||||
<small>Name</small>
|
||||
</th>
|
||||
<th colSpan={2} className="py-1 text-center">
|
||||
<small>Is Mandatory</small>
|
||||
</th>
|
||||
<th className="text-center py-1">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{checkListItems.map((item, index) => (
|
||||
<tr key={item.id} className="border-top-0">
|
||||
<td colSpan={2} className=" border-0">
|
||||
<input
|
||||
className="d-none"
|
||||
{...register(`checkList.${index}.id`)}
|
||||
></input>
|
||||
<input
|
||||
{...register(`checkList.${index}.description`)}
|
||||
className="form-control form-control-sm"
|
||||
placeholder={`Checklist item ${index + 1}`}
|
||||
onChange={(e) =>
|
||||
handleChecklistChange(index, e.target.value)
|
||||
}
|
||||
/>
|
||||
{errors.checkList?.[index]?.description && (
|
||||
<small
|
||||
style={{ fontSize: "10px" }}
|
||||
className="danger-text"
|
||||
>
|
||||
{errors.checkList[index]?.description?.message}
|
||||
</small>
|
||||
)}
|
||||
</td>
|
||||
<td colSpan={2} className="text-center border-0">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
{...register(`checkList.${index}.isMandatory`)}
|
||||
defaultChecked={item.isMandatory}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center border-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChecklistItem(index)}
|
||||
className="btn btn-xs btn-icon btn-text-secondary"
|
||||
|
||||
>
|
||||
<i className="bx bxs-minus-circle text-danger"data-bs-toggle="tooltip"
|
||||
title="Add Check"
|
||||
data-bs-original-title="Add check" ></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="table mt-1 border-0">
|
||||
<thead className="py-0 my-0 table-border-top-0">
|
||||
<tr className="py-1">
|
||||
<th colSpan={2} className="py-1">
|
||||
<small>Name</small>
|
||||
</th>
|
||||
<th colSpan={2} className="py-1 text-center">
|
||||
<small>Is Mandatory</small>
|
||||
</th>
|
||||
<th className="text-center py-1">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{checkListItems.map((item, index) => (
|
||||
<tr key={item.id} className="border-top-0">
|
||||
<td colSpan={2} className=" border-0">
|
||||
<input
|
||||
className="d-none"
|
||||
{...register(`checkList.${index}.id`)}
|
||||
></input>
|
||||
<input
|
||||
{...register(`checkList.${index}.description`)}
|
||||
className="form-control form-control-sm"
|
||||
placeholder={`Checklist item ${index + 1}`}
|
||||
onChange={(e) =>
|
||||
handleChecklistChange(index, e.target.value)
|
||||
}
|
||||
/>
|
||||
{errors.checkList?.[index]?.description && (
|
||||
<small
|
||||
style={{ fontSize: "10px" }}
|
||||
className="danger-text"
|
||||
>
|
||||
{errors.checkList[index]?.description?.message}
|
||||
</small>
|
||||
)}
|
||||
</td>
|
||||
<td colSpan={2} className="text-center border-0">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
{...register(`checkList.${index}.isMandatory`)}
|
||||
defaultChecked={item.isMandatory}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center border-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChecklistItem(index)}
|
||||
className="btn btn-xs btn-icon btn-text-secondary"
|
||||
|
||||
>
|
||||
<i className="bx bxs-minus-circle text-danger" data-bs-toggle="tooltip"
|
||||
title="Add Check"
|
||||
data-bs-original-title="Add check" ></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
type="button"
|
||||
className="btn btn-xs btn-primary mt-2"
|
||||
onClick={addChecklistItem}
|
||||
>
|
||||
<i className="bx bx-plus-circle" data-bs-toggle="tooltip"
|
||||
title="Add Check"
|
||||
data-bs-original-title="Add check" ></i>
|
||||
<i className="bx bx-plus-circle" data-bs-toggle="tooltip"
|
||||
title="Add Check"
|
||||
data-bs-original-title="Add check" ></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Submit / Cancel */}
|
||||
<div className="col-12 text-center mt-3">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
<div className="col-12 text-end mt-3">
|
||||
<button
|
||||
type="button" // ✅ change to button
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading ? "Please Wait" : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useUpdateContactCategory } from '../../hooks/masterHook/useMaster';
|
||||
import {useUpdateContactCategory} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -83,17 +84,17 @@ const EditContactCategory = ({ data, onClose }) => {
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Category Name</label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Category Name</Label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -111,20 +112,19 @@ const EditContactCategory = ({ data, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // clear inputs
|
||||
onClose?.(); // close modal from parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -6,7 +6,8 @@ import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
|
||||
import { getCachedData, cacheData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useUpdateContactTag } from '../../hooks/masterHook/useMaster';
|
||||
import {useUpdateContactTag} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
@ -83,17 +84,17 @@ const EditContactTag = ({ data, onClose }) => {
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Tag Name</label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Tag Name</Label>
|
||||
<input type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.name && <p className="text-danger">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -111,20 +112,19 @@ const EditContactTag = ({ data, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button" // ✅ not reset
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={() => {
|
||||
resetForm(); // clear inputs
|
||||
onClose?.(); // close modal from parent
|
||||
}}
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary ">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -5,7 +5,8 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { MasterRespository } from '../../repositories/MastersRepository';
|
||||
import { cacheData, getCachedData } from '../../slices/apiDataManager';
|
||||
import showToast from '../../services/toastService';
|
||||
import { useUpdateJobRole } from '../../hooks/masterHook/useMaster';
|
||||
import {useUpdateJobRole} from '../../hooks/masterHook/useMaster';
|
||||
import Label from '../common/Label';
|
||||
|
||||
|
||||
|
||||
@ -99,16 +100,16 @@ const EditJobRole = ({ data, onClose }) => {
|
||||
{/* <div className="col-12 col-md-12">
|
||||
<label className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">Edit Job Role</label>
|
||||
</div> */}
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Role</label>
|
||||
<input type="text"
|
||||
{...register("role")}
|
||||
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Role</Label>
|
||||
<input type="text"
|
||||
{...register("role")}
|
||||
className={`form-control ${errors.role ? 'is-invalids' : ''}`}
|
||||
/>
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -127,17 +128,19 @@ const EditJobRole = ({ data, onClose }) => {
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={onClose} // 👈 This will now close the popup
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading? "Please Wait...":"Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { MasterRespository } from "../../repositories/MastersRepository";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useUpdateApplicationRole } from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const updateSchema = z.object({
|
||||
role: z.string().min(1, { message: "Role is required" }),
|
||||
@ -162,8 +163,8 @@ const EditMaster = ({ master, onClose }) => {
|
||||
return (
|
||||
|
||||
<form className="row g-2 " onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Role</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Role</Label>
|
||||
<input type="text"
|
||||
{...register("role")}
|
||||
className={`form-control ${errors.role ? 'is-invalid' : ''}`}
|
||||
@ -171,8 +172,8 @@ const EditMaster = ({ master, onClose }) => {
|
||||
{errors.role && <p className="text-danger">{errors.role.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">Description</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>Description</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -275,15 +276,17 @@ const EditMaster = ({ master, onClose }) => {
|
||||
|
||||
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3"> {isLoading ? "Please Wait..." : "Submit"}</button>
|
||||
<div className="col-12 text-end mt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary"> {isLoading ? "Please Wait..." : "Submit"}</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -116,17 +116,18 @@ const EditWorkCategory = ({ data, onClose }) => {
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary me-3">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<div className="col-12 text-center mt-3">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary">
|
||||
{isLoading ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
156
src/components/master/ManageDocumentCategory.jsx
Normal file
156
src/components/master/ManageDocumentCategory.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useFeatures } from "../../hooks/useMasterRole";
|
||||
import { DOCUMENTS_ENTITIES, EXPENSE_MANAGEMENT } from "../../utils/constants";
|
||||
import {
|
||||
useCreateDocumentCatgory,
|
||||
useUpdateDocumentCategory,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
|
||||
export const Document_Entity = Object.entries(DOCUMENTS_ENTITIES).map(
|
||||
([key, value]) => ({ key, value })
|
||||
);
|
||||
|
||||
const ExpenseStatusSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
entityTypeId: z.string().min(1, { message: "Entity is required" }),
|
||||
});
|
||||
|
||||
const ManageDocumentCategory = ({ data, onClose }) => {
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(ExpenseStatusSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
entityTypeId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
|
||||
const { masterFeatures, loading } = useFeatures();
|
||||
|
||||
const ExpenseFeature = masterFeatures?.find(
|
||||
(appfeature) => appfeature.id === EXPENSE_MANAGEMENT
|
||||
);
|
||||
|
||||
const { mutate: CreateDocumentCategory, isPending } =
|
||||
useCreateDocumentCatgory(() => onClose?.());
|
||||
const { mutate: UpdateDocumentCategory, isPending: Updating } =
|
||||
useUpdateDocumentCategory(() => onClose?.());
|
||||
|
||||
const onSubmit = (payload) => {
|
||||
if (data) {
|
||||
UpdateDocumentCategory({
|
||||
id: data.id,
|
||||
payload: { ...payload, id: data.id },
|
||||
});
|
||||
} else {
|
||||
CreateDocumentCategory(payload);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data.name ?? "",
|
||||
description: data.description ?? "",
|
||||
entityTypeId: data.entityTypeId ?? "",
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
{loading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div>
|
||||
<div>
|
||||
<p className="fw-semibold">
|
||||
{data ? "Update Document Category" : "Add Document Category"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="row g-2 text-start"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Category Name</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`form-control form-control-sm `}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="danger-text">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">Select Entity</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("entityTypeId")}
|
||||
>
|
||||
<option value="" disabled>Select entity</option>
|
||||
{Document_Entity.map((entity) => (
|
||||
<option key={entity.key} value={entity.value}>
|
||||
{entity.key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.entityTypeId && (
|
||||
<p className="danger-text">{errors.entityTypeId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">Description</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
className={`form-control form-control-sm`}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="danger-text">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
disabled={isPending || Updating}
|
||||
>
|
||||
{isPending || Updating
|
||||
? "Please Wait..."
|
||||
: data
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={onClose}
|
||||
disabled={isPending || Updating}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDocumentCategory;
|
223
src/components/master/ManageDocumentType.jsx
Normal file
223
src/components/master/ManageDocumentType.jsx
Normal file
@ -0,0 +1,223 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCreateDocumentType, useDocumentCategories, useUpdateDocumentType } from "../../hooks/masterHook/useMaster";
|
||||
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
|
||||
|
||||
|
||||
export const Document_Entity = Object.entries(DOCUMENTS_ENTITIES).map(
|
||||
([key, value]) => ({ key, value })
|
||||
);
|
||||
const DocumentTypeSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
regexExpression: z.string().optional(),
|
||||
allowedContentType: z.string().min(1, { message: "Allowed content type is required" }),
|
||||
maxSizeAllowedInMB: z
|
||||
.number({ invalid_type_error: "Must be a number" })
|
||||
.min(1, { message: "Max size must be at least 1MB" }),
|
||||
isValidationRequired: z.boolean().default(true),
|
||||
isMandatory: z.boolean().default(true),
|
||||
documentCategoryId: z.string().min(1, { message: "Document Category is required" }),
|
||||
});
|
||||
|
||||
const ManageDocumentType = ({ data, onClose, documentCategories = [] }) => {
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(DocumentTypeSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
regexExpression: "",
|
||||
allowedContentType: "",
|
||||
maxSizeAllowedInMB: 25,
|
||||
isValidationRequired: true,
|
||||
isMandatory: true,
|
||||
documentCategoryId: "",
|
||||
entityTypeId:""
|
||||
},
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,watch,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
|
||||
|
||||
const selectedDocumentEntity = watch("entityTypeId")
|
||||
const {DocumentCategories,isLoading} = useDocumentCategories(selectedDocumentEntity)
|
||||
|
||||
|
||||
|
||||
const { mutate: createDocType, isPending: creating } = useCreateDocumentType(() =>
|
||||
onClose?.()
|
||||
);
|
||||
const { mutate: updateDocType, isPending: updating } = useUpdateDocumentType(() =>
|
||||
onClose?.()
|
||||
);
|
||||
const onSubmit = (payload) => {
|
||||
const { entityTypeId, ...rest } = payload;
|
||||
|
||||
if (data) {
|
||||
updateDocType({ id: data.id, payload: { ...rest, id: data.id } });
|
||||
} else {
|
||||
createDocType(rest);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data.name ?? "",
|
||||
regexExpression: data.regexExpression ?? "",
|
||||
allowedContentType: data.allowedContentType ?? "",
|
||||
maxSizeAllowedInMB: data.maxSizeAllowedInMB ?? 25,
|
||||
isValidationRequired: data.isValidationRequired ?? true,
|
||||
isMandatory: data.isMandatory ?? true,
|
||||
documentCategoryId: data.documentCategory?.id ?? "",
|
||||
entityTypeId:data.documentCategory?.entityTypeId ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="text-center">
|
||||
<p className="fw-semibold">
|
||||
{data ? "Edit Document Type" : "Add Document Type"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`form-control form-control-sm `}
|
||||
/>
|
||||
{errors.name && <a className="text-danger">{errors.name.message}</a>}
|
||||
</div>
|
||||
|
||||
{/* Regex Expression */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Regex Expression</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("regexExpression")}
|
||||
className="form-control form-control-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Allowed Content Type */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Allowed Content Type</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("allowedContentType")}
|
||||
className={`form-control form-control form-control-sm`}
|
||||
/>
|
||||
{errors.allowedContentType && (
|
||||
<a className="text-danger">{errors.allowedContentType.message}</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Max Size */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Max Size Allowed (MB)</label>
|
||||
<input
|
||||
type="number"
|
||||
{...register("maxSizeAllowedInMB", { valueAsNumber: true })}
|
||||
className={`form-control form-control-sm`}
|
||||
/>
|
||||
{errors.maxSizeAllowedInMB && (
|
||||
<a className="text-danger">{errors.maxSizeAllowedInMB.message}</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Validation Required */}
|
||||
<div className="col-6 form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
{...register("isValidationRequired")}
|
||||
/>
|
||||
<label className="form-check-label">Validation Required</label>
|
||||
</div>
|
||||
|
||||
{/* Mandatory */}
|
||||
<div className="col-6 form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input "
|
||||
{...register("isMandatory")}
|
||||
/>
|
||||
<label className="form-check-label">Mandatory</label>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label">Document Entity</label>
|
||||
<select
|
||||
{...register("entityTypeId")}
|
||||
className={`form-select form-select-sm`}
|
||||
>
|
||||
<option value="">-- Select Category --</option>
|
||||
{Document_Entity.map((entity) => (
|
||||
<option key={entity.key} value={entity.value}>
|
||||
{entity.key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.entityTypeId && (
|
||||
<a className="text-danger">{errors.entityTypeId.message}</a>
|
||||
)}
|
||||
</div>
|
||||
{/* Category */}
|
||||
<div className="col-12">
|
||||
<label className="form-label">Document Category</label>
|
||||
<select
|
||||
{...register("documentCategoryId")}
|
||||
className={`form-select form-select-sm`}
|
||||
>
|
||||
|
||||
{isLoading && <option value="" disabled>Loading....</option> }
|
||||
<option value="">-- Select Category --</option>
|
||||
{!isLoading && DocumentCategories?.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentCategoryId && (
|
||||
<a className="text-danger">{errors.documentCategoryId.message}</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="col-12 text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
disabled={creating || updating}
|
||||
>
|
||||
{creating || updating
|
||||
? "Please Wait..."
|
||||
: data
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={onClose}
|
||||
disabled={creating || updating}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDocumentType;
|
@ -6,6 +6,7 @@ import {
|
||||
useCreateExpenseType,
|
||||
useUpdateExpenseType,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const ExpnseSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
@ -52,8 +53,8 @@ const ManageExpenseType = ({ data = null, onClose }) => {
|
||||
}, [data]);
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Expesne Type Name</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Expesne Type Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("name")}
|
||||
@ -61,10 +62,10 @@ const ManageExpenseType = ({ data = null, onClose }) => {
|
||||
/>
|
||||
{errors.name && <p className="danger-text">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>
|
||||
Description
|
||||
</label>
|
||||
</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -83,10 +84,20 @@ const ManageExpenseType = ({ data = null, onClose }) => {
|
||||
{...register("noOfPersonsRequired")}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
disabled={isPending || isPendingUpdate}
|
||||
onClick={()=>onClose()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending || isPendingUpdate}
|
||||
>
|
||||
{isPending || isPendingUpdate
|
||||
@ -95,16 +106,7 @@ const ManageExpenseType = ({ data = null, onClose }) => {
|
||||
? "Update"
|
||||
: "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary "
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
disabled={isPending || isPendingUpdate}
|
||||
onClick={()=>onClose()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCreatePaymentMode, useUpdatePaymentMode } from "../../hooks/masterHook/useMaster";
|
||||
import Label from "../common/Label";
|
||||
|
||||
const ExpnseSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
@ -46,8 +47,8 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
|
||||
|
||||
return (
|
||||
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label">Payment Mode Name</label>
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" required>Payment Mode Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("name")}
|
||||
@ -55,10 +56,10 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
|
||||
/>
|
||||
{errors.name && <p className="danger-text">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="description">
|
||||
<div className="col-12 col-md-12 text-start">
|
||||
<Label className="form-label" htmlFor="description" required>
|
||||
Description
|
||||
</label>
|
||||
</Label>
|
||||
<textarea
|
||||
rows="3"
|
||||
{...register("description")}
|
||||
@ -70,23 +71,24 @@ const ManagePaymentMode = ({ data = null, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-center">
|
||||
<div className="col-12 text-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary me-3"
|
||||
disabled={isPending || Updating}
|
||||
>
|
||||
{isPending || Updating ? "Please Wait..." : Updating ? "Update" : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={onClose} // ✅ call onClose here
|
||||
type="reset"
|
||||
className="btn btn-sm btn-label-secondary me-3"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
disabled={isPending || Updating}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={isPending || Updating}
|
||||
>
|
||||
{isPending || Updating ? "Please Wait..." : Updating ? "Update" : "Submit"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -1,140 +1,68 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import CreateRole from "./CreateRole";
|
||||
import DeleteMaster from "./DeleteMaster";
|
||||
import EditRole from "./EditRole";
|
||||
import CreateJobRole from "./CreateJobRole";
|
||||
import EditJobRole from "./EditJobRole";
|
||||
import CreateActivity from "./CreateActivity";
|
||||
import EditActivity from "./EditActivity";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
import { MasterRespository } from "../../repositories/MastersRepository";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import showToast from "../../services/toastService";
|
||||
import CreateWorkCategory from "./CreateWorkCategory";
|
||||
import EditWorkCategory from "./EditWorkCategory";
|
||||
import CreateCategory from "./CreateContactCategory";
|
||||
import CreateContactTag from "./CreateContactTag";
|
||||
import EditContactCategory from "./EditContactCategory";
|
||||
import EditContactTag from "./EditContactTag";
|
||||
import { useDeleteMasterItem } from "../../hooks/masterHook/useMaster";
|
||||
import ManageExpenseType from "./ManageExpenseType";
|
||||
import ManagePaymentMode from "./ManagePaymentMode";
|
||||
import ManageExpenseStatus from "./ManageExpenseStatus";
|
||||
|
||||
import ManageDocumentCategory from "./ManageDocumentCategory";
|
||||
import ManageDocumentType from "./ManageDocumentType";
|
||||
|
||||
const MasterModal = ({ modaldata, closeModal }) => {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { mutate: deleteMasterItem, isPending } = useDeleteMasterItem();
|
||||
|
||||
const handleSelectedMasterDeleted = () => {
|
||||
const { masterType, item, validateFn } = modaldata || {};
|
||||
if (!masterType || !item?.id) {
|
||||
showToast("Missing master type or item", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteMasterItem(
|
||||
{ masterType, item, validateFn },
|
||||
{ onSuccess: handleCloseDeleteModal }
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseDeleteModal = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (modaldata?.modalType === "delete") {
|
||||
setIsDeleteModalOpen(true);
|
||||
}
|
||||
}, [modaldata]);
|
||||
|
||||
if (!modaldata?.modalType) {
|
||||
closeModal();
|
||||
if (!modaldata?.modalType || modaldata.modalType === "delete") {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (modaldata.modalType === "delete" && isDeleteModalOpen) {
|
||||
return (
|
||||
<div
|
||||
className="modal fade show"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type="delete"
|
||||
header={`Delete ${modaldata.masterType}`}
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleSelectedMasterDeleted}
|
||||
onClose={handleCloseDeleteModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { modalType, item, masterType } = modaldata;
|
||||
|
||||
const renderModalContent = () => {
|
||||
const { modalType, item, masterType } = modaldata;
|
||||
|
||||
const modalComponents = {
|
||||
"Application Role": <CreateRole masmodalType={masterType} onClose={closeModal} />,
|
||||
"Edit-Application Role": <EditRole master={modaldata} onClose={closeModal} />,
|
||||
"Job Role": <CreateJobRole onClose={closeModal} />,
|
||||
"Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />,
|
||||
"Activity": <CreateActivity onClose={closeModal} />,
|
||||
"Edit-Activity": <EditActivity activityData={item} onClose={closeModal} />,
|
||||
"Work Category": <CreateWorkCategory onClose={closeModal} />,
|
||||
"Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />,
|
||||
"Contact Category": <CreateCategory data={item} onClose={closeModal} />,
|
||||
"Edit-Contact Category": <EditContactCategory data={item} onClose={closeModal} />,
|
||||
"Contact Tag": <CreateContactTag data={item} onClose={closeModal} />,
|
||||
"Edit-Contact Tag": <EditContactTag data={item} onClose={closeModal} />,
|
||||
"Expense Type":<ManageExpenseType onClose={closeModal} />,
|
||||
"Edit-Expense Type":<ManageExpenseType data={item} onClose={closeModal} />,
|
||||
"Payment Mode":<ManagePaymentMode onClose={closeModal}/>,
|
||||
"Edit-Payment Mode":<ManagePaymentMode data={item} onClose={closeModal}/>,
|
||||
"Expense Status":<ManageExpenseStatus onClose={closeModal}/>,
|
||||
"Edit-Expense Status":<ManageExpenseStatus data={item} onClose={closeModal}/>
|
||||
};
|
||||
|
||||
return modalComponents[modalType] || null;
|
||||
const modalComponents = {
|
||||
"Application Role": (
|
||||
<CreateRole masmodalType={masterType} onClose={closeModal} />
|
||||
),
|
||||
"Edit-Application Role": (
|
||||
<EditRole master={modaldata} onClose={closeModal} />
|
||||
),
|
||||
"Job Role": <CreateJobRole onClose={closeModal} />,
|
||||
"Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />,
|
||||
"Activity": <CreateActivity onClose={closeModal} />,
|
||||
"Edit-Activity": <EditActivity activityData={item} onClose={closeModal} />,
|
||||
"Work Category": <CreateWorkCategory onClose={closeModal} />,
|
||||
"Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />,
|
||||
"Contact Category": <CreateCategory data={item} onClose={closeModal} />,
|
||||
"Edit-Contact Category": (
|
||||
<EditContactCategory data={item} onClose={closeModal} />
|
||||
),
|
||||
"Contact Tag": <CreateContactTag data={item} onClose={closeModal} />,
|
||||
"Edit-Contact Tag": <EditContactTag data={item} onClose={closeModal} />,
|
||||
"Expense Type": <ManageExpenseType onClose={closeModal} />,
|
||||
"Edit-Expense Type": <ManageExpenseType data={item} onClose={closeModal} />,
|
||||
"Payment Mode": <ManagePaymentMode onClose={closeModal} />,
|
||||
"Edit-Payment Mode": <ManagePaymentMode data={item} onClose={closeModal} />,
|
||||
"Expense Status": <ManageExpenseStatus onClose={closeModal} />,
|
||||
"Edit-Expense Status": (
|
||||
<ManageExpenseStatus data={item} onClose={closeModal} />
|
||||
),
|
||||
"Document Category": <ManageDocumentCategory onClose={closeModal} />,
|
||||
"Edit-Document Category": (
|
||||
<ManageDocumentCategory data={item} onClose={closeModal} />
|
||||
),
|
||||
"Document Type": <ManageDocumentType onClose={closeModal} />,
|
||||
"Edit-Document Type": (
|
||||
<ManageDocumentType data={item} onClose={closeModal} />
|
||||
),
|
||||
};
|
||||
|
||||
const isLargeModal = ["Application Role", "Edit-Application Role"].includes(
|
||||
modaldata.modalType
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal fade show"
|
||||
id="master-modal"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
aria-labelledby="modalToggleLabel"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<div className={`modal-dialog mx-sm-auto mx-1 ${isLargeModal ? "modal-lg" : "modal-md"} modal-simple`}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body p-sm-4 p-0">
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6>{modaldata?.modalType}</h6>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
{renderModalContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return modalComponents[modalType] || null;
|
||||
};
|
||||
|
||||
export default MasterModal;
|
||||
|
@ -171,6 +171,56 @@ const {
|
||||
|
||||
return { ExpenseStatus, loading, error };
|
||||
}
|
||||
export const useDocumentTypes =(category)=>{
|
||||
const {
|
||||
data: DocumentTypes = [],
|
||||
error,
|
||||
isError,
|
||||
isLoading
|
||||
} = useQuery({
|
||||
queryKey: ["Document Type",category],
|
||||
queryFn: async () => {
|
||||
const res = await MasterRespository.getDocumentTypes(category)
|
||||
return res.data;
|
||||
},
|
||||
enabled:!!category,
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Failed to fetch Expense Status",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return { DocumentTypes, isError, isLoading, error };
|
||||
}
|
||||
export const useDocumentCategories =(EntityType)=>{
|
||||
const {
|
||||
data: DocumentCategories = [],
|
||||
error,
|
||||
isError,
|
||||
isLoading
|
||||
} = useQuery({
|
||||
queryKey: ["Document Category",EntityType],
|
||||
queryFn: async () => {
|
||||
const res = await MasterRespository.getDocumentCategories(EntityType)
|
||||
return res.data;
|
||||
},
|
||||
enabled:!!EntityType,
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Failed to fetch Expense Status",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return { DocumentCategories, isError, isLoading, error };
|
||||
}
|
||||
// ===Application Masters Query=================================================
|
||||
|
||||
const fetchMasterData = async (masterType) => {
|
||||
@ -193,6 +243,10 @@ const fetchMasterData = async (masterType) => {
|
||||
return (await MasterRespository.getPaymentMode()).data;
|
||||
case "Expense Status":
|
||||
return (await MasterRespository.getExpenseStatus()).data;
|
||||
case "Document Type":
|
||||
return (await MasterRespository.getDocumentTypes()).data;
|
||||
case "Document Category":
|
||||
return (await MasterRespository.getDocumentCategories()).data;
|
||||
case "Status":
|
||||
return [
|
||||
{
|
||||
@ -634,6 +688,7 @@ export const useUpdatePaymentMode = (onSuccessCallback)=>{
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------Expense Status----------------------------------
|
||||
export const useCreateExpenseStatus =(onSuccessCallback)=>{
|
||||
const queryClient = useQueryClient();
|
||||
@ -677,6 +732,100 @@ export const useUpdateExpenseStatus = (onSuccessCallback)=>{
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------Document-Category--------------------------------
|
||||
export const useCreateDocumentCatgory =(onSuccessCallback)=>{
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation( {
|
||||
mutationFn: async ( payload ) =>
|
||||
{
|
||||
const resp = await MasterRespository.createDocumenyCategory(payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: ( data ) =>
|
||||
{
|
||||
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Category" ]} )
|
||||
queryClient.invalidateQueries( {queryKey:[ "Document Category" ]} )
|
||||
showToast( "Document Category added successfully", "success" );
|
||||
if(onSuccessCallback) onSuccessCallback(data)
|
||||
},
|
||||
onError: ( error ) =>
|
||||
{
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateDocumentCategory =(onSuccessCallback)=>{
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation( {
|
||||
mutationFn: async ( {id,payload} ) =>
|
||||
{
|
||||
const resp = await MasterRespository.updateDocumentCategory(id,payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: ( data ) =>
|
||||
{
|
||||
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Category" ]} )
|
||||
queryClient.invalidateQueries( {queryKey:[ "Document Category" ]} )
|
||||
showToast( "Document Category Updated successfully", "success" );
|
||||
if(onSuccessCallback) onSuccessCallback(data)
|
||||
},
|
||||
onError: ( error ) =>
|
||||
{
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------Document-Type-----------------------------------
|
||||
export const useCreateDocumentType =(onSuccessCallback)=>{
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation( {
|
||||
mutationFn: async ( payload ) =>
|
||||
{
|
||||
const resp = await MasterRespository.createDocumentType(payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: ( data ) =>
|
||||
{
|
||||
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Type" ]} )
|
||||
showToast( "Document Type added successfully", "success" );
|
||||
if(onSuccessCallback) onSuccessCallback(data)
|
||||
},
|
||||
onError: ( error ) =>
|
||||
{
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateDocumentType =(onSuccessCallback)=>{
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation( {
|
||||
mutationFn: async ( {id,payload} ) =>
|
||||
{
|
||||
const resp = await MasterRespository.updateDocumentType(id,payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: ( data ) =>
|
||||
{
|
||||
queryClient.invalidateQueries( {queryKey:[ "masterData", "Document Type" ]} )
|
||||
showToast( "Document Type Updated successfully", "success" );
|
||||
if(onSuccessCallback) onSuccessCallback(data)
|
||||
},
|
||||
onError: ( error ) =>
|
||||
{
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
}
|
||||
})
|
||||
}
|
||||
// -Delete Master --------
|
||||
export const useDeleteMasterItem = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
201
src/hooks/useDocument.js
Normal file
201
src/hooks/useDocument.js
Normal file
@ -0,0 +1,201 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import showToast from "../services/toastService";
|
||||
import { DocumentRepository } from "../repositories/DocumentRepository";
|
||||
|
||||
// ----------------------Query-------------------------------
|
||||
const cleanFilter = (filter) => {
|
||||
const cleaned = { ...filter };
|
||||
|
||||
[
|
||||
"uploadedByIds",
|
||||
"documentCategoryIds",
|
||||
"documentTypeIds",
|
||||
"documentTagIds",
|
||||
].forEach((key) => {
|
||||
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
|
||||
delete cleaned[key];
|
||||
}
|
||||
});
|
||||
|
||||
["startDate", "endDate", "isVerified"].forEach((key) => {
|
||||
if (cleaned[key] === null) {
|
||||
delete cleaned[key];
|
||||
}
|
||||
});
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
export const useDocumentListByEntityId = (
|
||||
entityTypeId,
|
||||
entityId,
|
||||
pageSize,
|
||||
pageNumber,
|
||||
filter,
|
||||
searchString = "",
|
||||
isActive
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"DocumentList",
|
||||
entityTypeId,
|
||||
entityId,
|
||||
pageSize,
|
||||
pageNumber,
|
||||
filter,
|
||||
searchString,
|
||||
isActive
|
||||
],
|
||||
queryFn: async () => {
|
||||
const cleanedFilter = cleanFilter(filter);
|
||||
const resp = await DocumentRepository.getDocumentList(
|
||||
entityTypeId,
|
||||
entityId,
|
||||
pageSize,
|
||||
pageNumber,
|
||||
cleanedFilter,
|
||||
searchString,
|
||||
isActive
|
||||
);
|
||||
return resp.data
|
||||
|
||||
},
|
||||
enabled: !!entityTypeId && !!entityId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDocumentFilterEntities = (entityTypeId) => {
|
||||
return useQuery({
|
||||
queryKey: ["DFilter", entityTypeId],
|
||||
queryFn: async () =>
|
||||
await DocumentRepository.getFilterEntities(entityTypeId),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDocumentDetails = (documentId) => {
|
||||
return useQuery({
|
||||
queryKey: ["Document", documentId],
|
||||
queryFn: async () => {
|
||||
const resp = await DocumentRepository.getDocumentById(documentId);
|
||||
return resp.data;
|
||||
},
|
||||
enabled: !!documentId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDocumentVersionList = (parentAttachmentId,pageSize,pageNumber) => {
|
||||
return useQuery({
|
||||
queryKey: ["DocumentVersionList", parentAttachmentId,pageSize,pageNumber],
|
||||
queryFn: async () => {
|
||||
const resp = await DocumentRepository.getDocumentVersionList(parentAttachmentId,pageSize,pageNumber);
|
||||
return resp.data
|
||||
},
|
||||
|
||||
enabled: !!parentAttachmentId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useDocumentVersion = (id)=>{
|
||||
return useQuery({
|
||||
queryKey:["DocumentVersion",id],
|
||||
queryFn:async()=> await DocumentRepository.getDocumentVersion(id),
|
||||
enabled:!!id
|
||||
})
|
||||
}
|
||||
|
||||
export const useDocumentTags =()=>{
|
||||
return useQuery({
|
||||
queryKey:["DocumentTag"],
|
||||
queryFn:async()=> {const resp = await DocumentRepository.getDocumentTags()
|
||||
return resp.data
|
||||
}
|
||||
})
|
||||
}
|
||||
//----------------------- MUTATION -------------------------
|
||||
|
||||
export const useUploadDocument = (onSuccessCallBack) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (DocumentPayload) =>
|
||||
DocumentRepository.uploadDocument(DocumentPayload),
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
|
||||
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
showToast(
|
||||
error.response.data.message ||
|
||||
"Something went wrong please try again !",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useUpdateDocument = (onSuccessCallBack) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ documentId, DocumentPayload }) =>
|
||||
DocumentRepository.UpdateDocument(documentId, DocumentPayload),
|
||||
onSuccess: (data, variables) => {
|
||||
const { documentId } = variables;
|
||||
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["Document", documentId] });
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
showToast(
|
||||
error.response.data.message ||
|
||||
"Something went wrong please try again !",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyDocument = ()=>{
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn:async({documentId,isVerify}) => await DocumentRepository.verifyDocument(documentId,isVerify),
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["DocumentVersionList"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["Document"] });
|
||||
showToast(
|
||||
data.response.data.message ||
|
||||
"Document Successfully Verified !",
|
||||
"success"
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.response.data.message ||
|
||||
"Something went wrong please try again !",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
export const useActiveInActiveDocument = ()=>{
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn:async({documentId,isActive}) => await DocumentRepository.deleteDocument(documentId,isActive),
|
||||
onSuccess: (data, variables) => {
|
||||
const {isActive} = variables;
|
||||
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
|
||||
showToast(`Document ${isActive ? "restored":"Deleted"} successfully`,"success")
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.response.data.message ||
|
||||
"Something went wrong please try again !",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
|
||||
})
|
||||
}
|
@ -14,7 +14,6 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import showToast from "../services/toastService";
|
||||
|
||||
|
||||
// ------------------------------Query-------------------
|
||||
|
||||
export const useProjects = () => {
|
||||
@ -153,7 +152,7 @@ export const useProjectName = () => {
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isError
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["basicProjectNameList"],
|
||||
queryFn: async () => {
|
||||
@ -164,7 +163,13 @@ export const useProjectName = () => {
|
||||
showToast(error.message || "Error while Fetching project Name", "error");
|
||||
},
|
||||
});
|
||||
return { projectNames: data, loading: isLoading, Error: error, refetch,isError };
|
||||
return {
|
||||
projectNames: data,
|
||||
loading: isLoading,
|
||||
Error: error,
|
||||
refetch,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProjectInfra = (projectId) => {
|
||||
@ -175,7 +180,7 @@ export const useProjectInfra = (projectId) => {
|
||||
} = useQuery({
|
||||
queryKey: ["ProjectInfra", projectId],
|
||||
queryFn: async () => {
|
||||
if(!projectId) return null;
|
||||
if (!projectId) return null;
|
||||
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
||||
return res.data;
|
||||
},
|
||||
@ -207,12 +212,7 @@ export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
|
||||
return { ProjectTaskList, isLoading, error };
|
||||
};
|
||||
|
||||
export const useProjectTasksByEmployee = (
|
||||
employeeId,
|
||||
fromDate,
|
||||
toDate,
|
||||
) => {
|
||||
|
||||
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
|
||||
return useQuery({
|
||||
queryKey: ["TasksByEmployee", employeeId],
|
||||
queryFn: async () => {
|
||||
@ -230,6 +230,43 @@ export const useProjectTasksByEmployee = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelEmployeeList = (ProjectId) => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelEmployeeList", ProjectId],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelEmployeeList(
|
||||
ProjectId
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
enabled: !!ProjectId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelModules = () => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelModules"],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelModules();
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelEmployeePermission = (employeeId, projectId) => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelEmployeePermission", employeeId, projectId],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelEmployeePermissions(
|
||||
employeeId,
|
||||
projectId
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
enabled: !!employeeId && !!projectId,
|
||||
});
|
||||
};
|
||||
|
||||
// -- -------------Mutation-------------------------------
|
||||
|
||||
export const useCreateProject = ({ onSuccessCallback }) => {
|
||||
@ -558,3 +595,26 @@ export const useDeleteProjectTask = (onSuccessCallback) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProjectLevelEmployeePermission = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload) =>
|
||||
await ProjectRepository.updateProjectLevelEmployeePermission(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ProjectLevelEmployeePermission"],
|
||||
});
|
||||
showToast("Permission Updated successfully", "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Failed to delete task",
|
||||
"error"
|
||||
);
|
||||
if (onSuccessCallback) onSuccessCallback();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
cacheData,
|
||||
clearCacheKey,
|
||||
@ -26,7 +26,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
const AttendancePage = () => {
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [ShowPending, setShowPending] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
const loginUser = getCachedProfileData();
|
||||
|
||||
@ -117,60 +117,70 @@ const AttendancePage = () => {
|
||||
{ label: "Attendance", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<div className="nav-align-top nav-tabs-shadow" >
|
||||
<ul className="nav nav-tabs" role="tablist">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
<div className="nav-align-top nav-tabs-shadow">
|
||||
{/* Tabs */}
|
||||
<div className="nav-align-top nav-tabs-shadow bg-white border-bottom">
|
||||
<div className="row align-items-center g-0 mb-3 mb-md-0">
|
||||
{/* Tabs */}
|
||||
<div className="col-12 col-md">
|
||||
<ul className="nav nav-tabs" role="tablist">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${
|
||||
activeTab === "all" ? "active" : ""
|
||||
} fs-6`}
|
||||
onClick={() => handleTabChange("all")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-home"
|
||||
>
|
||||
Today's
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabChange("all")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-home"
|
||||
>
|
||||
Today's
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${
|
||||
activeTab === "logs" ? "active" : ""
|
||||
} fs-6`}
|
||||
onClick={() => handleTabChange("logs")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-profile"
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
</li>
|
||||
<li className={`nav-item ${!DoRegularized && "d-none"}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabChange("logs")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-profile"
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${
|
||||
activeTab === "regularization" ? "active" : ""
|
||||
} fs-6`}
|
||||
onClick={() => handleTabChange("regularization")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-messages"
|
||||
>
|
||||
Regularization
|
||||
</button>
|
||||
</li>
|
||||
onClick={() => handleTabChange("regularization")}
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navs-top-messages"
|
||||
>
|
||||
Regularization
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 🔹 Search box placed after Regularization tab */}
|
||||
<li className="nav-item ms-auto me-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm mt-1"
|
||||
placeholder="Search Employee..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{ minWidth: "200px" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
{/* Single search input that moves */}
|
||||
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto px-2">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search Employee..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{ minWidth: "200px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tab-content attedanceTabs py-0 px-1 px-sm-3">
|
||||
{selectedProject ? (
|
||||
|
@ -173,12 +173,12 @@ const DailyTask = () => {
|
||||
currentSelectedBuilding={filters.selectedBuilding}
|
||||
currentSelectedFloors={filters.selectedFloors}
|
||||
currentSelectedActivities={filters.selectedActivities}
|
||||
selectedProject={selectedProject}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* --- Table --- */}
|
||||
<div className="table-responsive text-nowrap mt-3">
|
||||
<div className="table-responsive text-nowrap mt-3" style={{ minHeight: "200px" }}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -193,14 +193,16 @@ const DailyTask = () => {
|
||||
<tbody>
|
||||
{taskLoading && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">
|
||||
<Loader/>
|
||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px" }}>
|
||||
<Loader />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!taskLoading && groupedTasks.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">No Reports Found</td>
|
||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px" }}>
|
||||
No reports available for the selected date range.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!taskLoading &&
|
||||
@ -211,7 +213,7 @@ const DailyTask = () => {
|
||||
</tr>
|
||||
{tasks.map((task, idx) => (
|
||||
<tr key={task.id || idx}>
|
||||
<td className="flex-wrap text-start">
|
||||
<td className="flex-wrap text-start">
|
||||
<div>{task.workItem.activityMaster?.activityName || "No Activity Name"}</div>
|
||||
<div className="text-sm">
|
||||
{task.workItem.workArea?.floor?.building?.name} › {task.workItem.workArea?.floor?.floorName} › {task.workItem.workArea?.areaName}
|
||||
@ -239,6 +241,7 @@ const DailyTask = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -301,25 +301,15 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
</GlobalModel>
|
||||
)}
|
||||
{deleteContact && (
|
||||
<div
|
||||
className={`modal fade ${deleteContact ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: deleteContact ? "block" : "none",
|
||||
backgroundColor: deleteContact ? "rgba(0,0,0,0.5)" : "transparent",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Contact"}
|
||||
message={"Are you sure you want delete?"}
|
||||
onSubmit={handleDeleteContact}
|
||||
onClose={() => setDeleteContact(null)}
|
||||
loading={IsDeleting}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteContact}
|
||||
type="delete"
|
||||
header="Delete Contact"
|
||||
message="Are you sure you want delete?"
|
||||
onSubmit={handleDeleteContact}
|
||||
onClose={() => setDeleteContact(null)}
|
||||
loading={IsDeleting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{openBucketModal && (
|
||||
@ -383,12 +373,14 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
|
||||
{/* Empty state AFTER list */}
|
||||
{!loading && contacts?.length === 0 && (
|
||||
<p className="mt-3 ms-3 text-muted" >No contact found</p>
|
||||
<p className="mt-3 ms-3 text-muted">No contact found</p>
|
||||
)}
|
||||
{!loading &&
|
||||
contacts?.length > 0 &&
|
||||
currentItems.length === 0 && (
|
||||
<p className="mt-3 ms-3 text-muted">No matching contact found</p>
|
||||
<p className="mt-3 ms-3 text-muted">
|
||||
No matching contact found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -419,11 +411,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
{!loading && contacts?.length === 0 && (
|
||||
<p className="mt-3 ms-3 text-muted">No contact found</p>
|
||||
)}
|
||||
{!loading &&
|
||||
contacts?.length > 0 &&
|
||||
currentItems.length === 0 && (
|
||||
<p className="mt-3 ms-3 text-muted">No matching contact found</p>
|
||||
)}
|
||||
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
|
||||
<p className="mt-3 ms-3 text-muted">No matching contact found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -446,7 +436,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
currentItems.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page navigation">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
||||
<li
|
||||
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
@ -458,8 +450,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
{[...Array(totalPages)].map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
className={`page-item ${
|
||||
currentPage === index + 1 ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
@ -471,8 +464,9 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
))}
|
||||
|
||||
<li
|
||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
className={`page-item ${
|
||||
currentPage === totalPages ? "disabled" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className="page-link"
|
||||
@ -489,4 +483,4 @@ const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Directory;
|
||||
export default Directory;
|
||||
|
41
src/pages/Documents/DocumentPage.jsx
Normal file
41
src/pages/Documents/DocumentPage.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import Breadcrumb from '../../components/common/Breadcrumb'
|
||||
|
||||
const DocumentPage = () => {
|
||||
return (
|
||||
<div className=''>
|
||||
|
||||
<div className="card d-flex p-2">
|
||||
<div className="row align-items-center">
|
||||
{/* Search */}
|
||||
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
|
||||
<input
|
||||
type="search"
|
||||
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search Document"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="col-6 col-md-6 col-lg-9 text-end">
|
||||
<span className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer">
|
||||
Refresh
|
||||
< i className={`bx bx-refresh ms-1 `}></i>
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Add New Document"
|
||||
className="p-1 bg-primary rounded-circle cursor-pointer"
|
||||
>
|
||||
<i className="bx bx-plus fs-4 text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentPage
|
@ -44,7 +44,7 @@ const LoginPage = () => {
|
||||
localStorage.setItem("jwtToken", response.data.token);
|
||||
localStorage.setItem("refreshToken", response.data.refreshToken);
|
||||
setLoading(false);
|
||||
navigate("/");
|
||||
navigate("/dashboard");
|
||||
} else {
|
||||
await AuthRepository.sendOTP({ email: data.username });
|
||||
showToast("OTP has been sent to your email.", "success");
|
||||
|
@ -52,7 +52,7 @@ const LoginWithOtp = () => {
|
||||
setLoading(false);
|
||||
localStorage.removeItem("otpUsername");
|
||||
localStorage.removeItem("otpSentTime");
|
||||
navigate("/");
|
||||
navigate("/dashboard");
|
||||
|
||||
} catch (err) {
|
||||
showToast("Invalid or expired OTP.", "error");
|
||||
|
@ -79,11 +79,11 @@ const EmployeeList = () => {
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProjectId === null) {
|
||||
dispatch(setProjectId(projectNames[0]?.id));
|
||||
}
|
||||
}, [selectedProjectId]);
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId && projectNames?.length > 0) {
|
||||
dispatch(setProjectId(projectNames[0].id));
|
||||
}
|
||||
}, [selectedProjectId, projectNames, dispatch]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const applySearchFilter = (data, text) => {
|
||||
@ -177,10 +177,12 @@ const EmployeeList = () => {
|
||||
useEffect(() => {
|
||||
if (!loading && Array.isArray(employees)) {
|
||||
const sorted = [...employees].sort((a, b) => {
|
||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
|
||||
}`.toLowerCase();
|
||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
|
||||
}`.toLowerCase();
|
||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${
|
||||
a.lastName || ""
|
||||
}`.toLowerCase();
|
||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${
|
||||
b.lastName || ""
|
||||
}`.toLowerCase();
|
||||
return nameA?.localeCompare(nameB);
|
||||
});
|
||||
|
||||
@ -278,8 +280,9 @@ const EmployeeList = () => {
|
||||
? "Suspend Employee"
|
||||
: "Reactivate Employee"
|
||||
}
|
||||
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
||||
} this employee?`}
|
||||
message={`Are you sure you want to ${
|
||||
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
||||
} this employee?`}
|
||||
onSubmit={() =>
|
||||
suspendEmployee({
|
||||
employeeId: selectedEmpFordelete.id,
|
||||
@ -552,7 +555,7 @@ const EmployeeList = () => {
|
||||
colSpan={8}
|
||||
style={{ paddingTop: "20px", textAlign: "center" }}
|
||||
>
|
||||
No Data Found
|
||||
No team members assigned yet
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
@ -609,9 +612,10 @@ const EmployeeList = () => {
|
||||
{item.jobRole || "Not Assign Yet"}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className=" d-none d-md-table-cell">
|
||||
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
|
||||
{item.joiningDate
|
||||
? moment(item.joiningDate).format("DD-MMM-YYYY")
|
||||
: "NA"}
|
||||
</td>
|
||||
<td>
|
||||
{showInactive ? (
|
||||
|
@ -1,195 +1,138 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import MasterModal from "../../components/master/MasterModal";
|
||||
import { mastersList } from "../../data/masters";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||
import useMaster, { useMasterMenu } from "../../hooks/masterHook/useMaster"
|
||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||
import MasterTable from "./MasterTable";
|
||||
import { getCachedData } from "../../slices/apiDataManager";
|
||||
import useMaster, { useDeleteMasterItem, useMasterMenu } from "../../hooks/masterHook/useMaster";
|
||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { MANAGE_MASTER } from "../../utils/constants";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
|
||||
const MasterPage = () => {
|
||||
const {data,isLoading,isError,error:menuError} = useMasterMenu()
|
||||
const [modalConfig, setModalConfig] = useState({ modalType: "", item: null, masterType: null });
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filteredResults, setFilteredResults] = useState([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
const hasMasterPermission = useHasUserPermission(MANAGE_MASTER);
|
||||
const dispatch = useDispatch();
|
||||
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
||||
const queryClient = useQueryClient();
|
||||
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
||||
const hasMasterPermission = useHasUserPermission(MANAGE_MASTER);
|
||||
|
||||
const { data: masterData = [], loading, error, RecallApi,isError:isMasterError } = useMaster();
|
||||
const { data: menuData, isLoading: menuLoading, isError: menuErrorFlag, error: menuError } = useMasterMenu();
|
||||
const { data: masterData = [], loading, isError: isMasterError } = useMaster();
|
||||
const { mutate: DeleteMaster, isPending: isDeleting } = useDeleteMasterItem();
|
||||
|
||||
const openModal = () => setIsCreateModalOpen(true);
|
||||
const [modalConfig, setModalConfig] = useState(null);
|
||||
const [deleteData, setDeleteData] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const closeModal = () => {
|
||||
setIsCreateModalOpen(false);
|
||||
setModalConfig(null);
|
||||
|
||||
// Clean up Bootstrap modal manually
|
||||
const modalEl = document.getElementById('master-modal');
|
||||
modalEl?.classList.remove('show');
|
||||
if (modalEl) modalEl.style.display = 'none';
|
||||
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.overflow = 'auto';
|
||||
|
||||
document.querySelectorAll('.modal-backdrop').forEach((el) => el.remove());
|
||||
};
|
||||
|
||||
const handleModalData = (modalType, item, masterType = selectedMaster) => {
|
||||
setModalConfig({ modalType, item, masterType });
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const value = e.target.value.toLowerCase();
|
||||
setSearchTerm(value);
|
||||
|
||||
if (!masterData?.length) return;
|
||||
|
||||
const results = masterData.filter((item) =>
|
||||
Object.values(item).some(
|
||||
(field) => field?.toString().toLowerCase().includes(value)
|
||||
const displayData = useMemo(() => {
|
||||
const dataSource = queryClient.getQueryData(["masterData", selectedMaster]) || masterData;
|
||||
if (!searchTerm) return dataSource;
|
||||
return dataSource.filter((item) =>
|
||||
Object.values(item).some((val) =>
|
||||
val?.toString().toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
setFilteredResults(results);
|
||||
};
|
||||
const displayData = useMemo(() => {
|
||||
if (searchTerm) return filteredResults;
|
||||
return queryClient.getQueryData(["masterData", selectedMaster]) || masterData;
|
||||
}, [searchTerm, filteredResults, selectedMaster, masterData]);
|
||||
}, [searchTerm, masterData, selectedMaster, queryClient]);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
if (!displayData?.length) return [];
|
||||
return Object.keys(displayData[0]).map((key) => ({
|
||||
key,
|
||||
label: key.toUpperCase(),
|
||||
}));
|
||||
if (!displayData.length) return [];
|
||||
return Object.keys(displayData[0]).map((key) => ({ key, label: key.toUpperCase() }));
|
||||
}, [displayData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalConfig) openModal();
|
||||
}, [modalConfig]);
|
||||
const handleModalData = (type, item = null, masterType = selectedMaster) => {
|
||||
if (type === "delete") setDeleteData({ item, masterType });
|
||||
else setModalConfig({ modalType: type, item, masterType });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsCreateModalOpen(false);
|
||||
closeModal();
|
||||
};
|
||||
}, []);
|
||||
const handleDeleteSubmit = () => {
|
||||
if (!deleteData) return;
|
||||
DeleteMaster({ masterType: deleteData.masterType, item: deleteData.item }, {
|
||||
onSuccess: () => setDeleteData(null),
|
||||
});
|
||||
};
|
||||
|
||||
if (menuErrorFlag || isMasterError)
|
||||
return (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center py-5">
|
||||
<h4 className="mb-3">
|
||||
<i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred
|
||||
</h4>
|
||||
<p className="text-muted">{menuError?.message || "Error fetching master data"}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if(isError || isMasterError) return <div className="d-flex flex-column align-items-center justify-content-center py-5">
|
||||
<h4 className=" mb-3"><i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred</h4>
|
||||
<p className="text-muted">{error?.message || menuError?.message}</p>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
{isCreateModalOpen && (
|
||||
<MasterModal modaldata={modalConfig} closeModal={closeModal} />
|
||||
|
||||
{modalConfig && (
|
||||
<GlobalModel
|
||||
size={["Application Role", "Edit-Application Role"].includes(modalConfig.masterType) ? "lg" : "md"}
|
||||
isOpen={!!modalConfig}
|
||||
closeModal={() => setModalConfig(null)}
|
||||
>
|
||||
<MasterModal modaldata={modalConfig} closeModal={() => setModalConfig(null)} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
<ConfirmModal
|
||||
type="delete"
|
||||
header={`Delete ${selectedMaster}`}
|
||||
message={`Are you sure you want to delete this ${selectedMaster}?`}
|
||||
isOpen={!!deleteData}
|
||||
loading={isDeleting}
|
||||
onSubmit={handleDeleteSubmit}
|
||||
onClose={() => setDeleteData(null)}
|
||||
/>
|
||||
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Masters", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Masters" }]} />
|
||||
|
||||
<div className="row">
|
||||
<div className="card">
|
||||
<div className="card-datatable table-responsive py-4">
|
||||
<div
|
||||
id="DataTables_Table_0_wrapper"
|
||||
className="dataTables_wrapper dt-bootstrap5 no-footer"
|
||||
style={{ width: "98%" }}
|
||||
>
|
||||
<div className="row mb-2">
|
||||
<div className="col-md-3 col-sm-6">
|
||||
<div className="ms-0">
|
||||
<div
|
||||
className="dataTables_length text-start"
|
||||
id="DataTables_Table_0_length"
|
||||
>
|
||||
<label>
|
||||
<select
|
||||
onChange={(e) => dispatch(changeMaster(e.target.value))}
|
||||
name="DataTables_Table_0_length"
|
||||
aria-controls="DataTables_Table_0"
|
||||
className="form-select py-1 px-2"
|
||||
style={{ fontSize: "0.875rem", height: "32px", width: "150px" }}
|
||||
value={selectedMaster}
|
||||
>
|
||||
{isLoading && <option value="">Loading...</option>}
|
||||
{!isLoading &&
|
||||
data?.map((item) => (
|
||||
<option key={item.id} value={item.name}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-9 col-sm-6">
|
||||
<div className="dt-action-buttons text-xl-end text-lg-start text-md-end text-start d-flex align-items-center justify-content-sm-end justify-content-between gap-2 flex-md-row flex-sm-column mb-3 mb-md-0 mt-1 mt-md-0 gap-md-4">
|
||||
<div
|
||||
id="DataTables_Table_0_filter"
|
||||
className="dataTables_filter"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
type="search"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search"
|
||||
aria-controls="DataTables_Table_0"
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
></input>
|
||||
</label>
|
||||
</div>
|
||||
<div className={`dt-buttons btn-group flex-wrap ${!hasMasterPermission && 'd-none'}`}>
|
||||
{" "}
|
||||
<div className="input-group">
|
||||
|
||||
<button
|
||||
className={`btn btn-sm add-new btn-primary `}
|
||||
tabIndex="0"
|
||||
aria-controls="DataTables_Table_0"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#master-modal"
|
||||
onClick={() => {
|
||||
handleModalData(selectedMaster, "null", selectedMaster)
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
<span className=" d-sm-inline-block">
|
||||
Add {selectedMaster}
|
||||
</span>
|
||||
</span>
|
||||
</button>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mb-2">
|
||||
<div className="col-md-3 col-sm-6">
|
||||
<select
|
||||
className="form-select py-1 px-2"
|
||||
style={{ fontSize: "0.875rem", height: "32px", width: "190px" }}
|
||||
value={selectedMaster}
|
||||
onChange={(e) => dispatch(changeMaster(e.target.value))}
|
||||
>
|
||||
{menuLoading ? (
|
||||
<option value="">Loading...</option>
|
||||
) : (
|
||||
menuData?.map((item) => <option key={item.id} value={item.name}>{item.name}</option>)
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-9 col-sm-6 d-flex justify-content-end align-items-center gap-2">
|
||||
<div className="w-25"><input
|
||||
type="search"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/></div>
|
||||
{hasMasterPermission && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => handleModalData(selectedMaster, null, selectedMaster)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>Add {selectedMaster}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MasterTable data={displayData} columns={columns} loading={loading} handleModalData={handleModalData} />
|
||||
<div style={{ width: "1%" }}></div>
|
||||
</div>
|
||||
|
||||
<MasterTable
|
||||
data={displayData}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
handleModalData={handleModalData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
const selectedMaster = useSelector(
|
||||
(store) => store.localVariables.selectedMaster
|
||||
);
|
||||
const hiddenColumns = [
|
||||
const hiddenColumns = [
|
||||
"id",
|
||||
"featurePermission",
|
||||
"tenant",
|
||||
@ -20,7 +20,14 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
"noOfPersonsRequired",
|
||||
"color",
|
||||
"displayName",
|
||||
"permissionIds"
|
||||
"permissionIds",
|
||||
"entityTypeId",
|
||||
"regexExpression",
|
||||
"isMandatory",
|
||||
"maxFilesAllowed",
|
||||
"maxSizeAllowedInMB",
|
||||
"isValidationRequired",
|
||||
"documentCategory",
|
||||
];
|
||||
|
||||
const safeData = Array.isArray(data) ? data : [];
|
||||
@ -64,28 +71,35 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
: col.label,
|
||||
}));
|
||||
|
||||
const handleSystemDefined = (message) =>{
|
||||
if(message){
|
||||
showToast(`The system-defined item ${selectedMaster} cannot be ${message}.`)
|
||||
}
|
||||
const handleSystemDefined = (message) => {
|
||||
if (message) {
|
||||
showToast(
|
||||
`The system-defined item ${selectedMaster} cannot be ${message}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<table
|
||||
className="datatables-users table border-top dataTable no-footer dtr-column"
|
||||
id="DataTables_Table_0"
|
||||
aria-describedby="DataTables_Table_0_info"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<thead>
|
||||
<table className="datatables-users table border-top dataTable no-footer dtr-column w-100">
|
||||
<thead className="shadow-sm">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th className="text-start"> {selectedMaster === "Activity" ? "Activity" : "Name"}</th>
|
||||
<th className="text-start"> {selectedMaster === "Activity" ? "Unit" : "Description"}</th>
|
||||
<th className="text-start">
|
||||
{" "}
|
||||
{selectedMaster === "Activity" ? "Activity" : "Name"}
|
||||
</th>
|
||||
<th className="text-start">
|
||||
{" "}
|
||||
{selectedMaster === "Activity"
|
||||
? "Unit"
|
||||
: selectedMaster === "Document Type"
|
||||
? "Content Type"
|
||||
: "Description"}
|
||||
</th>
|
||||
<th className={` ${!hasMasterPermission && "d-none"}`}>
|
||||
Actions
|
||||
</th>
|
||||
@ -120,28 +134,28 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
</td>
|
||||
))}
|
||||
<td className={!hasMasterPermission ? "d-none" : ""}>
|
||||
{(selectedMaster === "Application Role" || selectedMaster === "Work Category") && item?.isSystem ? (
|
||||
{(selectedMaster === "Application Role" ||
|
||||
selectedMaster === "Work Category") &&
|
||||
item?.isSystem ? (
|
||||
<>
|
||||
<button
|
||||
aria-label="Modify"
|
||||
type="button"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
onClick={() =>
|
||||
handleSystemDefined("updated")
|
||||
}
|
||||
>
|
||||
<i className="bx bxs-edit me-2 text-primary"></i>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Modify"
|
||||
type="button"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
onClick={() => handleSystemDefined("updated")}
|
||||
>
|
||||
<i className="bx bxs-edit me-2 text-primary"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="Delete"
|
||||
type="button"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
onClick={() => handleSystemDefined("deleted")}
|
||||
>
|
||||
<i className="bx bx-trash me-1 text-danger"></i>
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
aria-label="Delete"
|
||||
type="button"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
onClick={() => handleSystemDefined("deleted")}
|
||||
>
|
||||
<i className="bx bx-trash me-1 text-danger"></i>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
@ -151,7 +165,11 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
data-bs-target="#master-modal"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
onClick={() =>
|
||||
handleModalData(`Edit-${selectedMaster}`, item)
|
||||
handleModalData(
|
||||
`Edit-${selectedMaster}`,
|
||||
item,
|
||||
selectedMaster
|
||||
)
|
||||
}
|
||||
>
|
||||
<i className="bx bxs-edit me-2 text-primary"></i>
|
||||
@ -161,9 +179,8 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
aria-label="Delete"
|
||||
type="button"
|
||||
className="btn p-0 dropdown-toggle hide-arrow"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#master-modal"
|
||||
onClick={() => handleModalData("delete", item)}
|
||||
onClick={() => handleModalData("delete", item, selectedMaster)}
|
||||
|
||||
>
|
||||
<i className="bx bx-trash me-1 text-danger"></i>
|
||||
</button>
|
||||
|
@ -16,9 +16,7 @@ import {
|
||||
useSelectedProject,
|
||||
} from "../../slices/apiDataManager";
|
||||
import "./ProjectDetails.css";
|
||||
import {
|
||||
useProjectDetails,
|
||||
} from "../../hooks/useProjects";
|
||||
import { useProjectDetails } from "../../hooks/useProjects";
|
||||
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
||||
import Directory from "../Directory/Directory";
|
||||
import eventBus from "../../services/eventBus";
|
||||
@ -26,19 +24,22 @@ import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChar
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
import ProjectDocument from "../../components/Project/ProjectDocuments";
|
||||
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
||||
import ProjectSetting from "../../components/Project/ProjectSetting";
|
||||
|
||||
const ProjectDetails = () => {
|
||||
|
||||
const projectId = useSelectedProject()
|
||||
|
||||
const { projectNames, fetchData } = useProjectName();
|
||||
const dispatch = useDispatch()
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId == null) {
|
||||
dispatch(setProjectId(projectNames[0]?.id));
|
||||
}
|
||||
}, [projectNames])
|
||||
}, [projectNames]);
|
||||
|
||||
const {
|
||||
projects_Details,
|
||||
@ -49,12 +50,15 @@ const ProjectDetails = () => {
|
||||
|
||||
// const [activePill, setActivePill] = useState("profile");
|
||||
const [activePill, setActivePill] = useState(() => {
|
||||
return localStorage.getItem("lastActiveProjectTab") || "profile";
|
||||
});
|
||||
return localStorage.getItem("lastActiveProjectTab") || "profile";
|
||||
});
|
||||
|
||||
const handler = useCallback(
|
||||
(msg) => {
|
||||
if (msg.keyword === "Update_Project" && projects_Details?.id === msg.response.id) {
|
||||
if (
|
||||
msg.keyword === "Update_Project" &&
|
||||
projects_Details?.id === msg.response.id
|
||||
) {
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
@ -66,11 +70,10 @@ const ProjectDetails = () => {
|
||||
return () => eventBus.off("project", handler);
|
||||
}, [handler]);
|
||||
|
||||
const handlePillClick = (pillKey) => {
|
||||
setActivePill(pillKey);
|
||||
localStorage.setItem("lastActiveProjectTab", pillKey); // ✅ Save to localStorage
|
||||
};
|
||||
|
||||
const handlePillClick = (pillKey) => {
|
||||
setActivePill(pillKey);
|
||||
localStorage.setItem("lastActiveProjectTab", pillKey); // ✅ Save to localStorage
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (projectLoading || !projects_Details) return <Loader />;
|
||||
@ -80,14 +83,19 @@ const ProjectDetails = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col-lg-4 col-md-5 mt-5">
|
||||
<div className="col-lg-4 col-md-5 mt-2">
|
||||
<AboutProject></AboutProject>
|
||||
<ProjectOverview project={projectId} />
|
||||
</div>
|
||||
<div className="col-lg-8 col-md-7 mt-5">
|
||||
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
|
||||
<div className="mt-5"> <AttendanceOverview /></div>
|
||||
|
||||
<ProjectProgressChart
|
||||
ShowAllProject="false"
|
||||
DefaultRange="1M"
|
||||
/>
|
||||
<div className="mt-5">
|
||||
{" "}
|
||||
<AttendanceOverview />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -103,14 +111,10 @@ const ProjectDetails = () => {
|
||||
);
|
||||
|
||||
case "infra":
|
||||
return (
|
||||
<ProjectInfra data={projects_Details} onDataChange={refetch} />
|
||||
);
|
||||
return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
|
||||
|
||||
case "workplan":
|
||||
return (
|
||||
<WorkPlan data={projects_Details} onDataChange={refetch} />
|
||||
);
|
||||
return <WorkPlan data={projects_Details} onDataChange={refetch} />;
|
||||
|
||||
case "directory":
|
||||
return (
|
||||
@ -118,6 +122,18 @@ const ProjectDetails = () => {
|
||||
<Directory IsPage={false} prefernceContacts={projects_Details.id} />
|
||||
</div>
|
||||
);
|
||||
case "documents":
|
||||
return (
|
||||
<div className="row">
|
||||
<ProjectDocuments />
|
||||
</div>
|
||||
);
|
||||
case "setting":
|
||||
return (
|
||||
<div className="row">
|
||||
<ProjectSetting />
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <ComingSoonPage />;
|
||||
@ -142,4 +158,4 @@ const ProjectDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetails;
|
||||
export default ProjectDetails;
|
||||
|
26
src/repositories/DocumentRepository.jsx
Normal file
26
src/repositories/DocumentRepository.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { api } from "../utils/axiosClient";
|
||||
|
||||
export const DocumentRepository = {
|
||||
uploadDocument:(data)=> api.post(`/api/Document/upload`,data),
|
||||
getDocumentList:(entityTypeId,entityId,pageSize, pageNumber, filter,searchString,isActive)=>{
|
||||
const payloadJsonString = JSON.stringify(filter);
|
||||
return api.get(`/api/Document/list/${entityTypeId}/entity/${entityId}/?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}&isActive=${isActive}`)
|
||||
},
|
||||
getDocumentById:(id)=>api.get(`/api/Document/get/details/${id}`),
|
||||
|
||||
getFilterEntities:(entityTypeId)=>api.get(`/api/Document/get/filter/${entityTypeId}`),
|
||||
|
||||
UpdateDocument:(documentId,data)=>api.put(`/api/Document/edit/${documentId}`,data),
|
||||
|
||||
getDocumentVersionList:(parentAttachmentId,pageSize,pageNumber)=>api.get(`/api/Document/list/versions/${parentAttachmentId}/?pageSize=${pageSize}&pageNumber=${pageNumber}`),
|
||||
|
||||
getDocumentVersion:(id)=>api.get(`/api/Document/get/version/${id}`),
|
||||
|
||||
verifyDocument:(id,isVerify)=>api.post(`/api/Document/verify/${id}/?isVerify=${isVerify}`),
|
||||
|
||||
deleteDocument:(id,isActive)=>api.delete(`/api/Document/delete/${id}/?isActive=${isActive}`),
|
||||
|
||||
getDocumentTags:()=>api.get('/api/Document/get/tags')
|
||||
|
||||
|
||||
}
|
@ -18,7 +18,7 @@ export const RolesRepository = {
|
||||
};
|
||||
|
||||
export const MasterRespository = {
|
||||
getMasterMenus:()=>api.get("/api/AppMenu/get/master-list"),
|
||||
getMasterMenus: () => api.get("/api/AppMenu/get/master-list"),
|
||||
|
||||
getRoles: () => api.get("/api/roles"),
|
||||
createRole: (data) => api.post("/api/roles", data),
|
||||
@ -48,6 +48,9 @@ export const MasterRespository = {
|
||||
api.delete(`/api/Master/payment-mode/delete/${id}`, (isActive = false)),
|
||||
"Expense Status": (id, isActive) =>
|
||||
api.delete(`/api/Master/expenses-status/delete/${id}`, (isActive = false)),
|
||||
"Document Type": (id) => api.delete(`/api/Master/document-type/delete/${id}`),
|
||||
"Document Category": (id) =>
|
||||
api.delete(`/api/Master/document-category/delete/${id}`),
|
||||
|
||||
getWorkCategory: () => api.get(`/api/master/work-categories`),
|
||||
createWorkCategory: (data) => api.post(`/api/master/work-category`, data),
|
||||
@ -81,4 +84,26 @@ export const MasterRespository = {
|
||||
createExpenseStatus: (data) => api.post("/api/Master/expenses-status", data),
|
||||
updateExepnseStatus: (id, data) =>
|
||||
api.put(`/api/Master/expenses-status/edit/${id}`, data),
|
||||
|
||||
getDocumentCategories: (entityType) =>
|
||||
api.get(
|
||||
`/api/Master/document-category/list${
|
||||
entityType ? `?entityTypeId=${entityType}` : ""
|
||||
}`
|
||||
),
|
||||
createDocumenyCategory: (data) =>
|
||||
api.post(`/api/Master/document-category`, data),
|
||||
updateDocumentCategory: (id, data) =>
|
||||
api.put(`/api/Master/document-category/edit/${id}`, data),
|
||||
|
||||
getDocumentTypes: (category) =>
|
||||
api.get(
|
||||
`/api/Master/document-type/list${
|
||||
category ? `?documentCategoryId=${category}` : ""
|
||||
}`
|
||||
),
|
||||
|
||||
createDocumentType: (data) => api.post(`/api/Master/document-type`, data),
|
||||
updateDocumentType: (id, data) =>
|
||||
api.put(`/api/Master/document-type/edit/${id}`, data),
|
||||
};
|
||||
|
@ -37,6 +37,14 @@ const ProjectRepository = {
|
||||
api.get(
|
||||
`/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}`
|
||||
),
|
||||
|
||||
|
||||
// Permission Managment for Employee at Project Level
|
||||
|
||||
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
|
||||
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
|
||||
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
|
||||
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data)
|
||||
};
|
||||
|
||||
export const TasksRepository = {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user