Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Payment_Getway_Management
This commit is contained in:
commit
3259a1ba81
@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Marco PMS</title>
|
<title>OnFieldWork.com</title>
|
||||||
|
|
||||||
<meta name="description" content="" />
|
<meta name="description" content="" />
|
||||||
|
|
||||||
|
|||||||
@ -442,4 +442,4 @@ font-weight: normal;
|
|||||||
.fs-md-large { font-size: 150% !important; }
|
.fs-md-large { font-size: 150% !important; }
|
||||||
.fs-md-xlarge { font-size: 170% !important; }
|
.fs-md-xlarge { font-size: 170% !important; }
|
||||||
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
|
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/img/app/dashboard-light-09.png
Normal file
BIN
public/img/app/dashboard-light-09.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
5
public/img/icons/diamond-info - Copy.svg
Normal file
5
public/img/icons/diamond-info - Copy.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.2" d="M46.5001 10.5288H32.5001L20.2251 26.5288L32.5001 56.5288L60.5001 26.5288L46.5001 10.5288Z" fill="#03C3EC"/>
|
||||||
|
<path d="M18.5 10.5288H46.5L60.5 26.5288L32.5 56.5288L4.5 26.5288L18.5 10.5288Z" stroke="#03C3EC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2934 9.92012C33.1042 9.67343 32.8109 9.52881 32.5 9.52881C32.1891 9.52881 31.8958 9.67343 31.7066 9.92012L19.7318 25.5288H4.5C3.94772 25.5288 3.5 25.9765 3.5 26.5288C3.5 27.0811 3.94772 27.5288 4.5 27.5288H19.5537L31.5745 56.9075C31.7282 57.2833 32.094 57.5288 32.5 57.5288C32.906 57.5288 33.2718 57.2833 33.4255 56.9075L45.4463 27.5288H60.5C61.0523 27.5288 61.5 27.0811 61.5 26.5288C61.5 25.9765 61.0523 25.5288 60.5 25.5288H45.2682L33.2934 9.92012ZM42.7474 25.5288L32.5 12.1717L22.2526 25.5288H42.7474ZM21.7146 27.5288L32.5 53.8881L43.2854 27.5288H21.7146Z" fill="#03C3EC"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.4 KiB |
@ -126,7 +126,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
|||||||
checked={ShowPending}
|
checked={ShowPending}
|
||||||
onChange={(e) => setShowPending(e.target.checked)}
|
onChange={(e) => setShowPending(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label ms-0">Show Pending</label>
|
<label className="form-check-label ms-0">Pending Attendance</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{attLoading ? (
|
{attLoading ? (
|
||||||
@ -223,50 +223,6 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
|
||||||
<nav aria-label="Page ">
|
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
|
||||||
<li
|
|
||||||
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link btn-xs"
|
|
||||||
onClick={() => paginate(currentPage - 1)}
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link "
|
|
||||||
onClick={() => paginate(index + 1)}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li
|
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link "
|
|
||||||
onClick={() => paginate(currentPage + 1)}
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -281,6 +237,48 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||||
|
<nav aria-label="Page ">
|
||||||
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
|
<li
|
||||||
|
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link btn-xs"
|
||||||
|
onClick={() => paginate(currentPage - 1)}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link "
|
||||||
|
onClick={() => paginate(index + 1)}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link "
|
||||||
|
onClick={() => paginate(currentPage + 1)}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository";
|
|||||||
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
import { useAttendancesLogs } from "../../hooks/useAttendance";
|
||||||
import { queryClient } from "../../layouts/AuthLayout";
|
import { queryClient } from "../../layouts/AuthLayout";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const usePagination = (data, itemsPerPage) => {
|
const usePagination = (data, itemsPerPage) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@ -38,162 +39,172 @@ const usePagination = (data, itemsPerPage) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showPending, setShowPending] = useState(false);
|
const [showPending, setShowPending] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
const isSameDay = (dateStr) => {
|
const isSameDay = (dateStr) => {
|
||||||
if (!dateStr) return false;
|
if (!dateStr) return false;
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
return d.getTime() === today.getTime();
|
return d.getTime() === today.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBeforeToday = (dateStr) => {
|
const isBeforeToday = (dateStr) => {
|
||||||
if (!dateStr) return false;
|
if (!dateStr) return false;
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
return d.getTime() < today.getTime();
|
return d.getTime() < today.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortByName = (a, b) => {
|
const sortByName = (a, b) => {
|
||||||
const nameA = (a.firstName + a.lastName).toLowerCase();
|
const nameA = (a.firstName + a.lastName).toLowerCase();
|
||||||
const nameB = (b.firstName + b.lastName).toLowerCase();
|
const nameB = (b.firstName + b.lastName).toLowerCase();
|
||||||
return nameA.localeCompare(nameB);
|
return nameA.localeCompare(nameB);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
|
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
|
||||||
selectedProject,
|
selectedProject,
|
||||||
dateRange.startDate,
|
dateRange.startDate,
|
||||||
dateRange.endDate,
|
dateRange.endDate,
|
||||||
organizationId
|
organizationId
|
||||||
);
|
|
||||||
|
|
||||||
const processedData = useMemo(() => {
|
|
||||||
const filteredData = showPending
|
|
||||||
? data.filter((item) => item.checkOutTime === null)
|
|
||||||
: data;
|
|
||||||
|
|
||||||
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
|
|
||||||
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
|
|
||||||
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
|
|
||||||
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
|
|
||||||
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
|
|
||||||
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
|
|
||||||
|
|
||||||
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
|
|
||||||
|
|
||||||
const groupedByDate = sortedList.reduce((acc, item) => {
|
|
||||||
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
|
||||||
if (date) {
|
|
||||||
acc[date] = acc[date] || [];
|
|
||||||
acc[date].push(item);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
|
|
||||||
return sortedDates.flatMap((date) => groupedByDate[date]);
|
|
||||||
}, [data, showPending]);
|
|
||||||
|
|
||||||
const filteredSearchData = useMemo(() => {
|
|
||||||
if (!searchTerm) return processedData;
|
|
||||||
|
|
||||||
const lowercased = searchTerm.toLowerCase();
|
|
||||||
return processedData.filter((item) =>
|
|
||||||
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
|
|
||||||
);
|
);
|
||||||
}, [processedData, searchTerm]);
|
|
||||||
|
|
||||||
const {
|
const processedData = useMemo(() => {
|
||||||
currentPage,
|
const filteredData = showPending
|
||||||
totalPages,
|
? data.filter((item) => item.checkOutTime === null)
|
||||||
currentItems: paginatedAttendances,
|
: data;
|
||||||
paginate,
|
|
||||||
resetPage,
|
|
||||||
} = usePagination(filteredSearchData, 20);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
|
||||||
resetPage();
|
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
|
||||||
}, [filteredSearchData]);
|
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
|
||||||
|
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
|
||||||
|
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
|
||||||
|
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
|
||||||
|
|
||||||
const handler = useCallback(
|
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
|
||||||
(msg) => {
|
|
||||||
const { startDate, endDate } = dateRange;
|
|
||||||
const checkIn = msg.response.checkInTime.substring(0, 10);
|
|
||||||
|
|
||||||
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) {
|
const groupedByDate = sortedList.reduce((acc, item) => {
|
||||||
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
|
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
|
||||||
if (!oldData) {
|
if (date) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
|
acc[date] = acc[date] || [];
|
||||||
return;
|
acc[date].push(item);
|
||||||
}
|
}
|
||||||
return oldData.map((record) =>
|
return acc;
|
||||||
record.id === msg.response.id ? { ...record, ...msg.response } : record
|
}, {});
|
||||||
);
|
|
||||||
});
|
|
||||||
resetPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[selectedProject, dateRange, resetPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
|
||||||
eventBus.on("attendance_log", handler);
|
return sortedDates.flatMap((date) => groupedByDate[date]);
|
||||||
return () => eventBus.off("attendance_log", handler);
|
}, [data, showPending]);
|
||||||
}, [handler]);
|
|
||||||
|
|
||||||
const employeeHandler = useCallback(
|
const filteredSearchData = useMemo(() => {
|
||||||
(msg) => {
|
if (!searchTerm) return processedData;
|
||||||
const { startDate, endDate } = dateRange;
|
|
||||||
if (data.some((item) => item.employeeId == msg.employeeId)) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data, refetch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const lowercased = searchTerm.toLowerCase();
|
||||||
eventBus.on("employee", employeeHandler);
|
return processedData.filter((item) =>
|
||||||
return () => eventBus.off("employee", employeeHandler);
|
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
|
||||||
}, [employeeHandler]);
|
);
|
||||||
|
}, [processedData, searchTerm]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
currentItems: paginatedAttendances,
|
||||||
|
paginate,
|
||||||
|
resetPage,
|
||||||
|
} = usePagination(filteredSearchData, 20);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetPage();
|
||||||
|
}, [filteredSearchData]);
|
||||||
|
|
||||||
|
const handler = useCallback(
|
||||||
|
(msg) => {
|
||||||
|
const { startDate, endDate } = dateRange;
|
||||||
|
const checkIn = msg.response.checkInTime.substring(0, 10);
|
||||||
|
|
||||||
|
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) {
|
||||||
|
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
|
||||||
|
if (!oldData) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return oldData.map((record) =>
|
||||||
|
record.id === msg.response.id ? { ...record, ...msg.response } : record
|
||||||
|
);
|
||||||
|
});
|
||||||
|
resetPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedProject, dateRange, resetPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eventBus.on("attendance_log", handler);
|
||||||
|
return () => eventBus.off("attendance_log", handler);
|
||||||
|
}, [handler]);
|
||||||
|
|
||||||
|
const employeeHandler = useCallback(
|
||||||
|
(msg) => {
|
||||||
|
const { startDate, endDate } = dateRange;
|
||||||
|
if (data.some((item) => item.employeeId == msg.employeeId)) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eventBus.on("employee", employeeHandler);
|
||||||
|
return () => eventBus.off("employee", employeeHandler);
|
||||||
|
}, [employeeHandler]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
className="dataTables_length text-start py-2 d-flex flex-wrap justify-content-between"
|
||||||
id="DataTables_Table_0_length"
|
id="DataTables_Table_0_length"
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-center my-0 ">
|
<div className="d-flex flex-wrap align-items-center gap-2 gap-md-3 my-0">
|
||||||
<DateRangePicker
|
{/* Date Range Picker */}
|
||||||
onRangeChange={setDateRange}
|
<div className="flex-grow-1 flex-md-grow-0">
|
||||||
defaultStartDate={yesterday}
|
<DateRangePicker
|
||||||
/>
|
onRangeChange={setDateRange}
|
||||||
<div className="form-check form-switch text-start ms-1 ms-md-2 align-items-center mb-0">
|
defaultStartDate={yesterday}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pending Attendance Switch */}
|
||||||
|
<div className="form-check form-switch text-start mb-0">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
role="switch"
|
role="switch"
|
||||||
disabled={isFetching}
|
|
||||||
id="inactiveEmployeesCheckbox"
|
id="inactiveEmployeesCheckbox"
|
||||||
|
disabled={isFetching}
|
||||||
checked={showPending}
|
checked={showPending}
|
||||||
onChange={(e) => setShowPending(e.target.checked)}
|
onChange={(e) => setShowPending(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label ms-0">Show Pending</label>
|
<label className="form-check-label ms-0 ms-md-0">
|
||||||
|
Pending Attendance
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="table-responsive text-nowrap"
|
className="table-responsive text-nowrap"
|
||||||
style={{ minHeight: "200px" }}
|
style={{ minHeight: "200px" }}
|
||||||
@ -232,9 +243,9 @@ useEffect(() => {
|
|||||||
const previousAttendance = arr[index - 1];
|
const previousAttendance = arr[index - 1];
|
||||||
const previousDate = previousAttendance
|
const previousDate = previousAttendance
|
||||||
? moment(
|
? moment(
|
||||||
previousAttendance.checkInTime ||
|
previousAttendance.checkInTime ||
|
||||||
previousAttendance.checkOutTime
|
previousAttendance.checkOutTime
|
||||||
).format("YYYY-MM-DD")
|
).format("YYYY-MM-DD")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!previousDate || currentDate !== previousDate) {
|
if (!previousDate || currentDate !== previousDate) {
|
||||||
@ -260,7 +271,12 @@ useEffect(() => {
|
|||||||
lastName={attendance.lastName}
|
lastName={attendance.lastName}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<a href="#" className="text-heading text-truncate">
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/employee/${attendance.employeeId}?for=attendance`)
|
||||||
|
}
|
||||||
|
className="text-heading text-truncate cursor-pointer"
|
||||||
|
>
|
||||||
<span className="fw-normal">
|
<span className="fw-normal">
|
||||||
{attendance.firstName} {attendance.lastName}
|
{attendance.firstName} {attendance.lastName}
|
||||||
</span>
|
</span>
|
||||||
@ -297,8 +313,7 @@ useEffect(() => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="my-12">
|
<div className="my-12">
|
||||||
<span className="text-secondary">
|
<span className="text-secondary">
|
||||||
No data available for the selected date range. Please Select
|
No attendance record found in selected date range.
|
||||||
another date.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -326,9 +341,8 @@ useEffect(() => {
|
|||||||
(pageNumber) => (
|
(pageNumber) => (
|
||||||
<li
|
<li
|
||||||
key={pageNumber}
|
key={pageNumber}
|
||||||
className={`page-item ${
|
className={`page-item ${currentPage === pageNumber ? "active" : ""
|
||||||
currentPage === pageNumber ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
@ -340,9 +354,8 @@ useEffect(() => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<li
|
<li
|
||||||
className={`page-item ${
|
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
||||||
currentPage === totalPages ? "disabled" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const InfraPlanning = () => {
|
|||||||
const selectedService = useCurrentService();
|
const selectedService = useCurrentService();
|
||||||
|
|
||||||
const { projectInfra, isLoading, isError, error, isFetched } =
|
const { projectInfra, isLoading, isError, error, isFetched } =
|
||||||
useProjectInfra(selectedProject, selectedService || "" );
|
useProjectInfra(selectedProject, selectedService || "");
|
||||||
|
|
||||||
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
||||||
@ -62,9 +62,13 @@ const InfraPlanning = () => {
|
|||||||
|
|
||||||
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div
|
||||||
<p className="my-3">No Result Found</p>
|
className="text-center d-flex justify-content-center align-items-center text-muted"
|
||||||
|
style={{ minHeight: "40vh", fontSize: "0.9rem" }}
|
||||||
|
>
|
||||||
|
<p className="my-3 m-0">No Result Found</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
} from "../../slices/apiDataManager";
|
} from "../../slices/apiDataManager";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const Regularization = ({
|
const Regularization = ({
|
||||||
handleRequest,
|
handleRequest,
|
||||||
@ -26,6 +27,7 @@ const Regularization = ({
|
|||||||
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const [regularizesList, setregularizedList] = useState([]);
|
const [regularizesList, setregularizedList] = useState([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
const { regularizes, loading, error, refetch } = useRegularizationRequests(
|
const { regularizes, loading, error, refetch } = useRegularizationRequests(
|
||||||
selectedProject,
|
selectedProject,
|
||||||
organizationId,
|
organizationId,
|
||||||
@ -33,9 +35,9 @@ const Regularization = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!regularizes) return
|
if (!regularizes) return
|
||||||
if(regularizes?.length) {
|
if (regularizes?.length) {
|
||||||
setregularizedList(regularizes);
|
setregularizedList(regularizes);
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [regularizes]);
|
}, [regularizes]);
|
||||||
@ -102,141 +104,106 @@ const Regularization = ({
|
|||||||
}, [employeeHandler]);
|
}, [employeeHandler]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div>
|
||||||
className="table-responsive text-nowrap pb-4"
|
<div
|
||||||
style={{ minHeight: "200px" }}
|
className="table-responsive pt-3 text-nowrap pb-4"
|
||||||
>
|
style={{ minHeight: "200px" }}
|
||||||
{loading ? (
|
>
|
||||||
<div
|
{loading ? (
|
||||||
className="d-flex justify-content-center align-items-center"
|
<div
|
||||||
style={{ height: "200px" }}
|
className="d-flex justify-content-center align-items-center"
|
||||||
>
|
style={{ height: "200px" }}
|
||||||
<p className="text-secondary">Loading...</p>
|
>
|
||||||
</div>
|
<p className="text-secondary">Loading...</p>
|
||||||
) : currentItems?.length > 0 ? (
|
</div>
|
||||||
<table className="table mb-0">
|
) : currentItems?.length > 0 ? (
|
||||||
<thead>
|
<table className="table mb-0">
|
||||||
<tr>
|
<thead>
|
||||||
<th colSpan={2}>Name</th>
|
<tr>
|
||||||
<th>Date</th>
|
<th colSpan={2}>Name</th>
|
||||||
<th>Organization</th>
|
<th>Date</th>
|
||||||
<th>
|
<th>Organization</th>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
<th>
|
||||||
</th>
|
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||||
<th>
|
</th>
|
||||||
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
<th>
|
||||||
</th>
|
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
|
||||||
<th colSpan={2}>
|
</th>
|
||||||
Requested By
|
<th colSpan={2}>
|
||||||
</th>
|
Requested By
|
||||||
<th >
|
</th>
|
||||||
Requested At
|
<th >
|
||||||
</th>
|
Requested At
|
||||||
<th>Action</th>
|
</th>
|
||||||
</tr>
|
<th>Action</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{currentItems?.map((att, index) => (
|
<tbody>
|
||||||
<tr key={index}>
|
{currentItems?.map((att, index) => (
|
||||||
<td colSpan={2}>
|
<tr key={index}>
|
||||||
<div className="d-flex justify-content-start align-items-center">
|
<td colSpan={2}>
|
||||||
<Avatar firstName={att.firstName} lastName={att.lastName} />
|
<div className="d-flex justify-content-start align-items-center">
|
||||||
<div className="d-flex flex-column">
|
<Avatar firstName={att.firstName} lastName={att.lastName} />
|
||||||
<a href="#" className="text-heading text-truncate">
|
<div className="d-flex flex-column"> <a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/employee/${att.employeeId}?for=attendance`)
|
||||||
|
}
|
||||||
|
className="text-heading text-truncate cursor-pointer" >
|
||||||
<span className="fw-normal">
|
<span className="fw-normal">
|
||||||
{att.firstName} {att.lastName}
|
{att.firstName} {att.lastName}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
|
||||||
|
|
||||||
<td>{att.organizationName || "--"}</td>
|
<td>{att.organizationName || "--"}</td>
|
||||||
|
|
||||||
<td>{convertShortTime(att.checkInTime)}</td>
|
<td>{convertShortTime(att.checkInTime)}</td>
|
||||||
<td>
|
|
||||||
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td colSpan={2}>
|
|
||||||
{att.requestedBy ? ( <div className="d-flex justify-content-start align-items-center">
|
|
||||||
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<a href="#" className="text-heading text-truncate">
|
|
||||||
<span className="fw-normal">
|
|
||||||
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>):(<small>--</small>)}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt,true) : "--"}
|
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center ">
|
|
||||||
<RegularizationActions
|
|
||||||
attendanceData={att}
|
|
||||||
handleRequest={handleRequest}
|
|
||||||
refresh={refetch}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) : (
|
|
||||||
<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 !"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* {!loading && totalPages > 1 && (
|
|
||||||
<nav aria-label="Page ">
|
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
|
|
||||||
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
|
|
||||||
<button
|
|
||||||
className="page-link btn-xs"
|
|
||||||
onClick={() => paginate(currentPage - 1)}
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link "
|
|
||||||
onClick={() => paginate(index + 1)}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li
|
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="page-link "
|
|
||||||
onClick={() => paginate(currentPage + 1)}
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
|
<td colSpan={2}>
|
||||||
|
{att.requestedBy ? (<div className="d-flex justify-content-start align-items-center">
|
||||||
|
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<a href="#" className="text-heading text-truncate">
|
||||||
|
<span className="fw-normal">
|
||||||
|
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>) : (<small>--</small>)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"}
|
||||||
|
</td>
|
||||||
|
<td className="text-center ">
|
||||||
|
<RegularizationActions
|
||||||
|
attendanceData={att}
|
||||||
|
handleRequest={handleRequest}
|
||||||
|
refresh={refetch}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<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 !"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{totalPages > 0 && (
|
{totalPages > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@ -244,6 +211,7 @@ const Regularization = ({
|
|||||||
onPageChange={paginate}
|
onPageChange={paginate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const HorizontalBarChart = ({
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
|
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
|
||||||
<span className="text-gray-500 text-sm">Loading chart...</span>
|
<span className="text-gray-500">Loading chart...</span>
|
||||||
{/* Replace this with a skeleton or spinner if you prefer */}
|
{/* Replace this with a skeleton or spinner if you prefer */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,7 +29,9 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = methods;
|
} = methods;
|
||||||
|
const closePanel = () => {
|
||||||
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
|
};
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
const filterPayload = {
|
const filterPayload = {
|
||||||
...formData,
|
...formData,
|
||||||
@ -37,12 +39,14 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
|
|||||||
dateTo: localToUtc(formData.dateTo),
|
dateTo: localToUtc(formData.dateTo),
|
||||||
};
|
};
|
||||||
handleFilter(filterPayload);
|
handleFilter(filterPayload);
|
||||||
|
closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
setResetKey((prev) => prev + 1);
|
setResetKey((prev) => prev + 1);
|
||||||
handleFilter(TaskReportDefaultValue);
|
handleFilter(TaskReportDefaultValue);
|
||||||
reset(TaskReportDefaultValue);
|
reset(TaskReportDefaultValue);
|
||||||
|
closePanel();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const TaskReportList = () => {
|
|||||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
||||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||||
|
|
||||||
const { service, openModal, closeModal,filter } = useDailyProgrssContext();
|
const { service, openModal, closeModal, filter } = useDailyProgrssContext();
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const { projectNames } = useProjectName();
|
const { projectNames } = useProjectName();
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ const TaskReportList = () => {
|
|||||||
selectedProject,
|
selectedProject,
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
currentPage,
|
currentPage,
|
||||||
service,filter
|
service, filter
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProgrssReportColumn = [
|
const ProgrssReportColumn = [
|
||||||
@ -192,109 +192,114 @@ const TaskReportList = () => {
|
|||||||
if (isLoading) return <TaskReportListSkeleton />;
|
if (isLoading) return <TaskReportListSkeleton />;
|
||||||
if (isError) return <div>Loading....</div>;
|
if (isError) return <div>Loading....</div>;
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 table-responsive text-nowrap">
|
<div>
|
||||||
<table className="table">
|
<div className="mt-2 table-responsive text-nowrap">
|
||||||
<thead>
|
<table className="table">
|
||||||
<tr>
|
<thead>
|
||||||
<th className="text-start">Activity</th>
|
|
||||||
<th>
|
|
||||||
<span>
|
|
||||||
Total Pending{" "}
|
|
||||||
<HoverPopup
|
|
||||||
title="Total Pending Task"
|
|
||||||
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
|
||||||
>
|
|
||||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
|
||||||
</HoverPopup>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<span>
|
|
||||||
Reported/Planned{" "}
|
|
||||||
<HoverPopup
|
|
||||||
title="Reported and Planned Task"
|
|
||||||
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
|
||||||
>
|
|
||||||
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
|
||||||
</HoverPopup>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th>Assign Date</th>
|
|
||||||
<th>Team</th>
|
|
||||||
<th className="text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{groupedTasks.length === 0 && (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
|
<th className="text-start">Activity</th>
|
||||||
No reports available
|
<th>
|
||||||
</td>
|
<span>
|
||||||
|
Total Pending{" "}
|
||||||
|
<HoverPopup
|
||||||
|
title="Total Pending Task"
|
||||||
|
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
||||||
|
>
|
||||||
|
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||||
|
</HoverPopup>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span>
|
||||||
|
Reported/Planned{" "}
|
||||||
|
<HoverPopup
|
||||||
|
title="Reported and Planned Task"
|
||||||
|
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
||||||
|
>
|
||||||
|
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||||
|
</HoverPopup>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>Assign Date</th>
|
||||||
|
<th>Team</th>
|
||||||
|
<th className="text-center">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
|
<tbody>
|
||||||
{groupedTasks.map(({ date, tasks }) => (
|
{groupedTasks.length === 0 && (
|
||||||
<React.Fragment key={date}>
|
<tr>
|
||||||
<tr className="table-row-header text-start">
|
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
|
||||||
<td colSpan={6}>
|
No reports available
|
||||||
<strong>{formatUTCToLocalTime(date)}</strong>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{tasks.map((task, idx) => (
|
)}
|
||||||
<tr key={task.id || idx}>
|
|
||||||
<td className="flex-wrap text-start">
|
{groupedTasks.map(({ date, tasks }) => (
|
||||||
<div>
|
<React.Fragment key={date}>
|
||||||
{task.workItem.activityMaster?.activityName || "No Activity Name"}
|
<tr className="table-row-header text-start">
|
||||||
</div>
|
<td colSpan={6}>
|
||||||
<div className="text-sm py-2">
|
<strong>{formatUTCToLocalTime(date)}</strong>
|
||||||
{task.workItem.workArea?.floor?.building?.name} ›{" "}
|
|
||||||
{task.workItem.workArea?.floor?.floorName} ›{" "}
|
|
||||||
{task.workItem.workArea?.areaName}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{formatNumber(task.workItem.plannedWork)}
|
|
||||||
</td>
|
|
||||||
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
|
|
||||||
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
|
||||||
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<div className="d-flex justify-content-end gap-2">
|
|
||||||
{ReportTaskRights && !task.reportedDate && (
|
|
||||||
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
|
|
||||||
Report
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-warning"
|
|
||||||
onClick={() => openModal("comments", { task, isActionAllow: true })}
|
|
||||||
>
|
|
||||||
QC
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-primary"
|
|
||||||
onClick={() => openModal("comments", { task, isActionAllow: false })}
|
|
||||||
>
|
|
||||||
Comment
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
{tasks.map((task, idx) => (
|
||||||
</React.Fragment>
|
<tr key={task.id || idx}>
|
||||||
))}
|
<td className="flex-wrap text-start">
|
||||||
</tbody>
|
<div>
|
||||||
</table>
|
{task.workItem.activityMaster?.activityName || "No Activity Name"}
|
||||||
{data?.data?.length > 0 && (
|
</div>
|
||||||
<Pagination
|
<div className="text-sm py-2">
|
||||||
currentPage={currentPage}
|
{task.workItem.workArea?.floor?.building?.name} ›{" "}
|
||||||
totalPages={data.totalPages}
|
{task.workItem.workArea?.floor?.floorName} ›{" "}
|
||||||
onPageChange={paginate}
|
{task.workItem.workArea?.areaName}
|
||||||
/>
|
</div>
|
||||||
)}
|
</td>
|
||||||
</div>
|
<td>
|
||||||
|
{formatNumber(task.workItem.plannedWork)}
|
||||||
|
</td>
|
||||||
|
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
|
||||||
|
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||||
|
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<div className="d-flex justify-content-end gap-2">
|
||||||
|
{ReportTaskRights && !task.reportedDate && (
|
||||||
|
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
|
||||||
|
Report
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-warning"
|
||||||
|
onClick={() => openModal("comments", { task, isActionAllow: true })}
|
||||||
|
>
|
||||||
|
QC
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-primary"
|
||||||
|
onClick={() => openModal("comments", { task, isActionAllow: false })}
|
||||||
|
>
|
||||||
|
Comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
data?.data?.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,11 @@ import ProjectCompletionChart from "./ProjectCompletionChart";
|
|||||||
import ProjectProgressChart from "./ProjectProgressChart";
|
import ProjectProgressChart from "./ProjectProgressChart";
|
||||||
import ProjectOverview from "../Project/ProjectOverview";
|
import ProjectOverview from "../Project/ProjectOverview";
|
||||||
import AttendanceOverview from "./AttendanceChart";
|
import AttendanceOverview from "./AttendanceChart";
|
||||||
|
import ExpenseAnalysis from "./ExpenseAnalysis";
|
||||||
|
import ExpenseStatus from "./ExpenseStatus";
|
||||||
|
import ExpenseByProject from "./ExpenseByProject";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { projectsCardData } = useDashboardProjectsCardData();
|
|
||||||
const { teamsCardData } = useDashboardTeamsCardData();
|
|
||||||
const { tasksCardData } = useDashboardTasksCardData();
|
|
||||||
|
|
||||||
// Get the selected project ID from Redux store
|
// Get the selected project ID from Redux store
|
||||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||||
@ -29,16 +29,16 @@ const Dashboard = () => {
|
|||||||
<div className="row gy-4">
|
<div className="row gy-4">
|
||||||
{isAllProjectsSelected && (
|
{isAllProjectsSelected && (
|
||||||
<div className="col-sm-6 col-lg-4">
|
<div className="col-sm-6 col-lg-4">
|
||||||
<Projects projectsCardData={projectsCardData} />
|
<Projects />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||||
<Teams teamsCardData={teamsCardData} />
|
<Teams />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||||
<TasksCard tasksCardData={tasksCardData} />
|
<TasksCard/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAllProjectsSelected && (
|
{isAllProjectsSelected && (
|
||||||
@ -56,11 +56,25 @@ const Dashboard = () => {
|
|||||||
<div className="col-xxl-6 col-lg-6">
|
<div className="col-xxl-6 col-lg-6">
|
||||||
<ProjectProgressChart />
|
<ProjectProgressChart />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-xl-8">
|
||||||
|
<div className="card h-100">
|
||||||
|
<ExpenseAnalysis />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-xl-4 col-md-6">
|
||||||
|
<div className="card ">
|
||||||
|
<ExpenseStatus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{!isAllProjectsSelected && (
|
{!isAllProjectsSelected && (
|
||||||
<div className="col-xxl-6 col-lg-6">
|
<div className="col-12 col-md-6 mb-sm-0 mb-4 ">
|
||||||
<AttendanceOverview /> {/* ✅ Removed unnecessary projectId prop */}
|
<AttendanceOverview />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<ExpenseByProject />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
110
src/components/Dashboard/DashboardSkeleton.jsx
Normal file
110
src/components/Dashboard/DashboardSkeleton.jsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: "linear-gradient(90deg, #eee, #f5f5f5, #eee)",
|
||||||
|
backgroundSize: "200% 100%",
|
||||||
|
animation: "skeleton-loading 1.5s infinite",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const skeletonStyle = `
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectCardSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Inject animation CSS once */}
|
||||||
|
<style>{skeletonStyle}</style>
|
||||||
|
|
||||||
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
|
<h5 className="fw-bold mb-0 ms-2">
|
||||||
|
<i className="rounded-circle bx bx-building-house text-primary"></i>{" "}
|
||||||
|
Projects
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skeleton body */}
|
||||||
|
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="40px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="40px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{skeletonStyle}</style>
|
||||||
|
|
||||||
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
|
<h5 className="fw-bold mb-0 ms-2">
|
||||||
|
<i className="bx bx-group text-warning"></i> Teams
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skeleton Body */}
|
||||||
|
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="90px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="70px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const TasksSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{skeletonStyle}</style>
|
||||||
|
|
||||||
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
|
<h5 className="fw-bold mb-0 ms-2">
|
||||||
|
<i className="bx bx-task text-success"></i> Tasks
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skeleton Body */}
|
||||||
|
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="70px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
|
||||||
|
<SkeletonLine height={14} width="90px" className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
167
src/components/Dashboard/ExpenseAnalysis.jsx
Normal file
167
src/components/Dashboard/ExpenseAnalysis.jsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
import { useExpenseAnalysis } from "../../hooks/useDashboard_Data";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { formatCurrency, localToUtc } from "../../utils/appUtils";
|
||||||
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
|
|
||||||
|
const ExpenseAnalysis = () => {
|
||||||
|
const projectId = useSelectedProject();
|
||||||
|
const [projectName, setProjectName] = useState("All Project");
|
||||||
|
const { projectNames, loading } = useProjectName();
|
||||||
|
|
||||||
|
const methods = useForm({
|
||||||
|
defaultValues: { startDate: "", endDate: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId && projectNames?.length) {
|
||||||
|
const project = projectNames.find((p) => p.id === projectId);
|
||||||
|
setProjectName(project?.name || "All Project");
|
||||||
|
} else {
|
||||||
|
setProjectName("All Project");
|
||||||
|
}
|
||||||
|
}, [projectNames, projectId]);
|
||||||
|
|
||||||
|
const { watch } = methods;
|
||||||
|
const [startDate, endDate] = watch(["startDate", "endDate"]);
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis(
|
||||||
|
projectId,
|
||||||
|
startDate ? localToUtc(startDate) : null,
|
||||||
|
endDate ? localToUtc(endDate) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
|
||||||
|
const report = data?.report ?? [];
|
||||||
|
const { labels, series, total } = useMemo(() => {
|
||||||
|
const labels = report.map((item) => item.projectName);
|
||||||
|
const series = report.map((item) => item.totalApprovedAmount || 0);
|
||||||
|
const total = formatCurrency(data?.totalAmount || 0);
|
||||||
|
return { labels, series, total };
|
||||||
|
}, [report, data?.totalAmount]);
|
||||||
|
|
||||||
|
const donutOptions = {
|
||||||
|
chart: { type: "donut" },
|
||||||
|
labels,
|
||||||
|
legend: { show: false },
|
||||||
|
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
|
||||||
|
colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
|
||||||
|
plotOptions: {
|
||||||
|
pie: {
|
||||||
|
donut: {
|
||||||
|
size: "70%",
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
total: {
|
||||||
|
show: true,
|
||||||
|
label: "Total",
|
||||||
|
fontSize: "16px",
|
||||||
|
formatter: () => `${total}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responsive: [
|
||||||
|
{
|
||||||
|
breakpoint: 576, // mobile breakpoint
|
||||||
|
options: {
|
||||||
|
chart: { width: "100%" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
|
||||||
|
<div className="text-start w-100">
|
||||||
|
<h5 className="mb-1 card-title">Expense Breakdown</h5>
|
||||||
|
{/* <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p> */}
|
||||||
|
<p className="card-subtitle m-0">{projectName}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-start text-sm-end w-75">
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<DateRangePicker1 />
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card body */}
|
||||||
|
<div className="card-body position-relative">
|
||||||
|
{isLoading && (
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-center align-items-center"
|
||||||
|
style={{ height: "200px" }}
|
||||||
|
>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && report.length === 0 && (
|
||||||
|
<div className="text-center py-5 text-muted">No data found</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && report.length > 0 && (
|
||||||
|
<>
|
||||||
|
{isFetching && (
|
||||||
|
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-center mb-3">
|
||||||
|
<Chart
|
||||||
|
options={donutOptions}
|
||||||
|
series={series}
|
||||||
|
type="donut"
|
||||||
|
width="100%"
|
||||||
|
height={320}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2 w-100">
|
||||||
|
<div className="row g-2">
|
||||||
|
{report.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
className="col-12 col-sm-6 d-flex align-items-start"
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
<div className="avatar me-2">
|
||||||
|
<span
|
||||||
|
className="avatar-initial rounded-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
donutOptions.colors[idx % donutOptions.colors.length],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bx bx-receipt fs-4"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column gap-1 text-start">
|
||||||
|
<small className="fw-semibold">{item.projectName}</small>
|
||||||
|
<span className="fw-semibold text-muted ms-1">
|
||||||
|
{formatCurrency(item.totalApprovedAmount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseAnalysis;
|
||||||
178
src/components/Dashboard/ExpenseByProject.jsx
Normal file
178
src/components/Dashboard/ExpenseByProject.jsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
import { useExpenseType } from "../../hooks/masterHook/useMaster";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useExpenseDataByProject } from "../../hooks/useDashboard_Data";
|
||||||
|
import { formatCurrency } from "../../utils/appUtils";
|
||||||
|
import { formatDate_DayMonth } from "../../utils/dateUtils";
|
||||||
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
|
||||||
|
const ExpenseByProject = () => {
|
||||||
|
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||||
|
const [projectName, setProjectName] = useState("All Project");
|
||||||
|
const [range, setRange] = useState("12M");
|
||||||
|
const { projectNames, loading } = useProjectName();
|
||||||
|
const [selectedType, setSelectedType] = useState("");
|
||||||
|
const [viewMode, setViewMode] = useState("Category");
|
||||||
|
const [chartData, setChartData] = useState({ categories: [], data: [] });
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
|
||||||
|
const { ExpenseTypes, loading: typeLoading } = useExpenseType();
|
||||||
|
|
||||||
|
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
|
||||||
|
projectId,
|
||||||
|
selectedType,
|
||||||
|
range === "All" ? null : parseInt(range)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedProject && projectNames?.length) {
|
||||||
|
const project = projectNames.find((p) => p.id === selectedProject);
|
||||||
|
setProjectName(project?.name || "All Project");
|
||||||
|
} else {
|
||||||
|
setProjectName("All Project");
|
||||||
|
}
|
||||||
|
}, [projectNames, selectedProject]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expenseApiData) {
|
||||||
|
const categories = expenseApiData.map((item) =>
|
||||||
|
formatDate_DayMonth(item.monthName, item.year)
|
||||||
|
);
|
||||||
|
const data = expenseApiData.map((item) => item.total);
|
||||||
|
setChartData({ categories, data });
|
||||||
|
} else {
|
||||||
|
setChartData({ categories: [], data: [] });
|
||||||
|
}
|
||||||
|
}, [expenseApiData]);
|
||||||
|
|
||||||
|
const getSelectedTypeName = () => {
|
||||||
|
if (!selectedType) return "All Types";
|
||||||
|
const found = ExpenseTypes.find((t) => t.id === selectedType);
|
||||||
|
return found ? found.name : "All Types";
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
chart: { type: "bar", toolbar: { show: false } },
|
||||||
|
plotOptions: {
|
||||||
|
bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 },
|
||||||
|
},
|
||||||
|
dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) },
|
||||||
|
xaxis: {
|
||||||
|
categories: chartData.categories,
|
||||||
|
labels: { style: { fontSize: "12px" }, rotate: -45 },
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] },
|
||||||
|
fill: { opacity: 1 },
|
||||||
|
colors: ["#2196f3"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: getSelectedTypeName(),
|
||||||
|
data: chartData.data,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card shadow-sm rounded ">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="d-flex justify-content-start align-items-center mb-3 mt-3">
|
||||||
|
<div className="text-start">
|
||||||
|
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
|
||||||
|
<p className="card-subtitle m-0">{projectName}</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-group mb-4 ms-n8">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm dropdown-toggle fs-5"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{viewMode}
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end ">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
setViewMode("Category");
|
||||||
|
setSelectedType("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Category
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
setViewMode("Project");
|
||||||
|
setSelectedType("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Project
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Range Buttons + Expense Dropdown */}
|
||||||
|
<div className="d-flex align-items-center flex-wrap ">
|
||||||
|
{["1M", "3M", "6M", "12M", "All"].map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
className={`border-0 px-2 py-1 text-sm rounded ${range === item
|
||||||
|
? "text-white bg-primary"
|
||||||
|
: "text-body bg-transparent"
|
||||||
|
}`}
|
||||||
|
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
||||||
|
onClick={() => setRange(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{viewMode === "Category" && (
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm ms-auto mb-3 mt-1 mt-sm-0"
|
||||||
|
value={selectedType}
|
||||||
|
onChange={(e) => setSelectedType(e.target.value)}
|
||||||
|
disabled={typeLoading}
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
|
>
|
||||||
|
<option value="">All Types</option>
|
||||||
|
{ExpenseTypes.map((type) => (
|
||||||
|
<option key={type.id} value={type.id}>
|
||||||
|
{type.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart */}
|
||||||
|
<div className="card-body bg-white text-dark p-3 rounded" style={{ minHeight: "210px" }}>
|
||||||
|
{isLoading ? (
|
||||||
|
<p>Loading chart...</p>
|
||||||
|
) : !expenseApiData || expenseApiData.length === 0 ? (
|
||||||
|
<div className="text-center text-muted py-5">No data found</div>
|
||||||
|
) : (
|
||||||
|
<Chart options={options} series={series} type="bar" height={235} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseByProject;
|
||||||
157
src/components/Dashboard/ExpenseStatus.jsx
Normal file
157
src/components/Dashboard/ExpenseStatus.jsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useExpense } from "../../hooks/useExpense";
|
||||||
|
import { useExpenseStatus } from "../../hooks/useDashboard_Data";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
|
import { countDigit, formatCurrency } from "../../utils/appUtils";
|
||||||
|
import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
|
||||||
|
|
||||||
|
const ExpenseStatus = () => {
|
||||||
|
const [projectName, setProjectName] = useState("All Project");
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const { projectNames, loading } = useProjectName();
|
||||||
|
const { data, isPending, error } = useExpenseStatus(selectedProject);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isManageExpense = useHasUserPermission(EXPENSE_MANAGE)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedProject && projectNames?.length) {
|
||||||
|
const project = projectNames.find((p) => p.id === selectedProject);
|
||||||
|
setProjectName(project?.name || "All Project");
|
||||||
|
} else {
|
||||||
|
setProjectName("All Project");
|
||||||
|
}
|
||||||
|
}, [projectNames, selectedProject]);
|
||||||
|
|
||||||
|
const handleNavigate = (status) => {
|
||||||
|
if (selectedProject) {
|
||||||
|
navigate(`/expenses/${status}/${selectedProject}`);
|
||||||
|
} else {
|
||||||
|
navigate(`/expenses/${status}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card-header d-flex justify-content-between text-start ">
|
||||||
|
<div className="m-0">
|
||||||
|
<h5 className="card-title mb-1">Expense - By Status</h5>
|
||||||
|
<p className="card-subtitle m-0 ">{projectName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-body ">
|
||||||
|
|
||||||
|
<div className="report-list text-start">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "Pending Payment",
|
||||||
|
count: data?.processPending?.count || 0,
|
||||||
|
amount: data?.processPending?.totalAmount || 0,
|
||||||
|
icon: "bx bx-rupee",
|
||||||
|
iconColor: "text-primary",
|
||||||
|
status: EXPENSE_STATUS.payment_pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pending Approve",
|
||||||
|
count: data?.approvePending?.count || 0,
|
||||||
|
amount: data?.approvePending?.totalAmount || 0,
|
||||||
|
icon: "fa-solid fa-check",
|
||||||
|
iconColor: "text-warning",
|
||||||
|
status: EXPENSE_STATUS.approve_pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pending Review",
|
||||||
|
count: data?.reviewPending?.count || 0,
|
||||||
|
amount: data?.reviewPending?.totalAmount || 0,
|
||||||
|
icon: "bx bx-search-alt-2",
|
||||||
|
iconColor: "text-secondary",
|
||||||
|
status: EXPENSE_STATUS.review_pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Draft",
|
||||||
|
count: data?.draft?.count || 0,
|
||||||
|
amount: data?.draft?.totalAmount || 0,
|
||||||
|
icon: "bx bx-file-blank",
|
||||||
|
iconColor: "text-info",
|
||||||
|
status: EXPENSE_STATUS.daft,
|
||||||
|
},
|
||||||
|
].map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="report-list-item rounded-2 mb-4 bg-lighter px-2 py-1 cursor-pointer"
|
||||||
|
onClick={() => handleNavigate(item?.status)}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div className="report-list-icon shadow-xs me-2">
|
||||||
|
<span className="d-inline-flex align-items-center justify-content-center rounded-circle border p-2">
|
||||||
|
<i className={`${item?.icon} ${item?.iconColor} bx-lg`}></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-between align-items-center w-100 flex-wrap gap-2">
|
||||||
|
<div className="d-flex flex-column gap-2">
|
||||||
|
<span className="fw-bold">{item?.title}</span>
|
||||||
|
{item?.amount ? (
|
||||||
|
<small className="mb-0 text-primary">
|
||||||
|
{formatCurrency(item?.amount)}
|
||||||
|
</small>
|
||||||
|
) : (
|
||||||
|
<small className="mb-0 text-primary">{formatCurrency(0)}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small
|
||||||
|
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
|
||||||
|
} text-gray-500`}
|
||||||
|
>
|
||||||
|
{item?.count || 0}
|
||||||
|
</small>
|
||||||
|
<small className="text-muted fs-semibold text-royalblue text-md">
|
||||||
|
<i className="bx bx-chevron-right"></i>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=" py-0 text-start mb-2">
|
||||||
|
{isManageExpense && (
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-between align-items-center cursor-pointer"
|
||||||
|
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
|
||||||
|
>
|
||||||
|
<div className="d-block">
|
||||||
|
<span
|
||||||
|
className={`fs-semibold d-block ${countDigit(data?.totalAmount || 0) > 3 ? "text-base" : "text-lg"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Project Spendings:
|
||||||
|
</span>{" "}
|
||||||
|
<small className="d-block text-xxs text-gary-80">
|
||||||
|
(All Processed Payments)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
|
||||||
|
} text-md`}
|
||||||
|
>
|
||||||
|
{formatCurrency(data?.totalAmount || 0)}
|
||||||
|
</span>
|
||||||
|
<small className="text-muted fs-semibold text-royalblue text-md">
|
||||||
|
<i className="bx bx-chevron-right"></i>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseStatus;
|
||||||
@ -3,7 +3,8 @@ import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
|||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
|
|
||||||
const ProjectCompletionChart = () => {
|
const ProjectCompletionChart = () => {
|
||||||
const { projects, loading } = useProjects();
|
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
|
||||||
|
|
||||||
|
|
||||||
// Bar chart logic
|
// Bar chart logic
|
||||||
const projectNames = projects?.map((p) => p.name) || [];
|
const projectNames = projects?.map((p) => p.name) || [];
|
||||||
@ -11,7 +12,7 @@ const ProjectCompletionChart = () => {
|
|||||||
projects?.map((p) => {
|
projects?.map((p) => {
|
||||||
const completed = p.completedWork || 0;
|
const completed = p.completedWork || 0;
|
||||||
const planned = p.plannedWork || 1;
|
const planned = p.plannedWork || 1;
|
||||||
const percent = (completed / planned) * 100;
|
const percent = planned ? (completed / planned) * 100 : 0;
|
||||||
return Math.min(Math.round(percent), 100);
|
return Math.min(Math.round(percent), 100);
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
|
import ProjectInfra from "../Project/ProjectInfra";
|
||||||
|
import { ProjectCardSkeleton } from "./DashboardSkeleton";
|
||||||
|
import { formatFigure } from "../../utils/appUtils";
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const {
|
const {
|
||||||
@ -8,6 +11,7 @@ const Projects = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
|
isFetching,
|
||||||
refetch,
|
refetch,
|
||||||
} = useDashboardProjectsCardData();
|
} = useDashboardProjectsCardData();
|
||||||
|
|
||||||
@ -23,7 +27,7 @@ const Projects = () => {
|
|||||||
|
|
||||||
const totalProjects = projectsCardData?.totalProjects ?? 0;
|
const totalProjects = projectsCardData?.totalProjects ?? 0;
|
||||||
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
|
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
|
||||||
|
if (isLoading) return <ProjectCardSkeleton />;
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
@ -33,24 +37,41 @@ const Projects = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isError ? (
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<div className="d-flex flex-column justify-content-center align-items-center p-1">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<i className="bx bx-error-circle bx-sm fs-2 "></i>
|
||||||
<span className="visually-hidden">Loading...</span>
|
<small className="text-muted mb-2">
|
||||||
</div>
|
{error?.message || "Unable to load data at the moment."}
|
||||||
</div>
|
</small>
|
||||||
) : isError ? (
|
<span
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
className={`text-muted ${
|
||||||
{error?.message || "Error loading data"}
|
isFetching ? "cursor-wait" : "cursor-pointer"
|
||||||
|
}`}
|
||||||
|
onClick={refetch}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
|
||||||
|
></i>{" "}
|
||||||
|
Retry
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">{totalProjects.toLocaleString()}</h4>
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{formatFigure(totalProjects ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<small className="text-muted">Total</small>
|
<small className="text-muted">Total</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">{ongoingProjects.toLocaleString()}</h4>
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{formatFigure(ongoingProjects ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
<small className="text-muted">Ongoing</small>
|
<small className="text-muted">Ongoing</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
||||||
|
import { TasksSkeleton } from "./DashboardSkeleton";
|
||||||
|
import { formatCurrency, formatFigure } from "../../utils/appUtils";
|
||||||
|
|
||||||
const TasksCard = () => {
|
const TasksCard = () => {
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
@ -10,42 +12,57 @@ const TasksCard = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
} = useDashboardTasksCardData(projectId);
|
} = useDashboardTasksCardData(projectId);
|
||||||
|
if (isLoading) return <TasksSkeleton />;
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex flex-column justify-content-between">
|
||||||
|
{/* Header */}
|
||||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
<h5 className="fw-bold mb-0 ms-2">
|
<h5 className="fw-bold mb-0 ms-2">
|
||||||
<i className="bx bx-task text-success"></i> Tasks
|
<i className="bx bx-task text-success"></i> Tasks
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isError ? (
|
||||||
// Loader while fetching
|
<div className="d-flex flex-column justify-content-center align-items-center p-3">
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<i className="bx bx-error-circle bx-sm fs-2 mb-2"></i>
|
||||||
<div className="spinner-border text-primary" role="status">
|
<small className="text-muted mb-2">
|
||||||
<span className="visually-hidden">Loading...</span>
|
{error?.message || "Unable to load data at the moment."}
|
||||||
</div>
|
</small>
|
||||||
</div>
|
<span
|
||||||
) : isError ? (
|
className={`text-muted ${
|
||||||
// Show error
|
isFetching ? "cursor-wait" : "cursor-pointer"
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
}`}
|
||||||
{error?.message || "Error loading data"}
|
onClick={refetch}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
|
||||||
|
></i>
|
||||||
|
Retry
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Show data
|
<div className="d-flex justify-content-around align-items-start flex-wrap mt-n2">
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
{/* Total Tasks */}
|
||||||
<div>
|
<div className="text-center flex-fill p-2">
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold text-truncate">
|
||||||
{tasksCardData?.totalTasks?.toLocaleString() ?? 0}
|
{formatFigure(tasksCardData?.totalTasks ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
</h4>
|
</h4>
|
||||||
<small className="text-muted">Total</small>
|
<small className="text-muted d-block">Total</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h4 className="mb-0 fw-bold">
|
{/* Completed Tasks */}
|
||||||
{tasksCardData?.completedTasks?.toLocaleString() ?? 0}
|
<div className="text-center flex-fill p-2">
|
||||||
|
<h4 className="mb-0 fw-bold text-truncate">
|
||||||
|
{formatFigure(tasksCardData?.completedTasks ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
</h4>
|
</h4>
|
||||||
<small className="text-muted">Completed</small>
|
<small className="text-muted d-block">Completed</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,16 +4,20 @@ import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
|
|||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { TeamsSkeleton } from "./DashboardSkeleton";
|
||||||
|
import { formatFigure } from "../../utils/appUtils";
|
||||||
|
|
||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const projectId = useSelectedProject()
|
const projectId = useSelectedProject();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: teamsCardData,
|
data: teamsCardData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
} = useDashboardTeamsCardData(projectId);
|
} = useDashboardTeamsCardData(projectId);
|
||||||
|
|
||||||
// Handle real-time updates via eventBus
|
// Handle real-time updates via eventBus
|
||||||
@ -40,6 +44,7 @@ const Teams = () => {
|
|||||||
const inToday = teamsCardData?.inToday ?? 0;
|
const inToday = teamsCardData?.inToday ?? 0;
|
||||||
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
|
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
|
||||||
|
|
||||||
|
if (isLoading) return <TeamsSkeleton />;
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
@ -48,24 +53,41 @@ const Teams = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isError ? (
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<div className="d-flex flex-column justify-content-center align-items-center p-1">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<i className="bx bx-error-circle bx-sm fs-2 "></i>
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
<small className="text-muted mb-2">
|
||||||
</div>
|
{error?.message || "Unable to load data at the moment."}
|
||||||
) : isError ? (
|
</small>
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
<span
|
||||||
{error?.message || "Error loading data"}
|
className={`text-muted ${
|
||||||
|
isFetching ? "cursor-wait" : "cursor-pointer"
|
||||||
|
}`}
|
||||||
|
onClick={refetch}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bx bx-refresh me-1 ${isFetching ? "bx-spin" : ""}`}
|
||||||
|
></i>{" "}
|
||||||
|
Retry
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4>
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{formatFigure(totalEmployees ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
<small className="text-muted">Total Employees</small>
|
<small className="text-muted">Total Employees</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">{inToday.toLocaleString()}</h4>
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{formatFigure(inToday ?? 0, {
|
||||||
|
notation: "compact",
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
<small className="text-muted">In Today</small>
|
<small className="text-muted">In Today</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
56
src/components/Directory/ContactFilterChips.jsx
Normal file
56
src/components/Directory/ContactFilterChips.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
|
||||||
|
const data = filterData?.data || filterData || {};
|
||||||
|
|
||||||
|
const filterChips = useMemo(() => {
|
||||||
|
const chips = [];
|
||||||
|
|
||||||
|
const addGroup = (ids, list, label, key) => {
|
||||||
|
if (!ids?.length) return;
|
||||||
|
const items = ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
name: list?.find((i) => i.id === id)?.name || id,
|
||||||
|
}));
|
||||||
|
chips.push({ key, label, items });
|
||||||
|
};
|
||||||
|
|
||||||
|
addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds");
|
||||||
|
addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds");
|
||||||
|
|
||||||
|
return chips;
|
||||||
|
}, [filters, filterData]);
|
||||||
|
|
||||||
|
if (!filterChips.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
{filterChips.map((chipGroup) => (
|
||||||
|
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
|
||||||
|
<span className="fw-semibold me-2">{chipGroup.label}:</span>
|
||||||
|
{chipGroup.items.map((item) => (
|
||||||
|
<span
|
||||||
|
key={item.id}
|
||||||
|
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
|
||||||
|
>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white btn-sm ms-2"
|
||||||
|
style={{
|
||||||
|
filter: "invert(1) grayscale(1)",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
}}
|
||||||
|
onClick={() => removeFilterChip(chipGroup.key, item.id)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactFilterChips;
|
||||||
@ -160,8 +160,7 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<i
|
<i
|
||||||
className={`bx ${
|
className={`bx ${isPending && activeContact === row.id
|
||||||
isPending && activeContact === row.id
|
|
||||||
? "bx-loader-alt bx-spin"
|
? "bx-loader-alt bx-spin"
|
||||||
: "bx-recycle"
|
: "bx-recycle"
|
||||||
} me-1 text-primary cursor-pointer`}
|
} me-1 text-primary cursor-pointer`}
|
||||||
@ -188,13 +187,13 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{Pagination && (
|
|
||||||
<div className="d-flex justify-content-start">
|
|
||||||
{Pagination}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{Pagination && (
|
||||||
|
<div className="d-flex justify-content-start">
|
||||||
|
{Pagination}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
79
src/components/Directory/NoteFilterChips.jsx
Normal file
79
src/components/Directory/NoteFilterChips.jsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||||
|
// Normalize data (in case it’s wrapped in .data)
|
||||||
|
const data = filterData?.data || filterData || {};
|
||||||
|
|
||||||
|
const filterChips = useMemo(() => {
|
||||||
|
const chips = [];
|
||||||
|
|
||||||
|
const buildGroup = (ids, list, label, key) => {
|
||||||
|
if (!ids?.length) return;
|
||||||
|
const items = ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
name: list?.find((item) => item.id === id)?.name || id,
|
||||||
|
}));
|
||||||
|
chips.push({ key, label, items });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build chips dynamically
|
||||||
|
buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds");
|
||||||
|
buildGroup(filters.organizations, data.organizations, "Organization", "organizations");
|
||||||
|
|
||||||
|
// Example: Add date range if you ever add in future
|
||||||
|
if (filters.startDate || filters.endDate) {
|
||||||
|
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
|
||||||
|
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
|
||||||
|
chips.push({
|
||||||
|
key: "dateRange",
|
||||||
|
label: "Date Range",
|
||||||
|
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chips;
|
||||||
|
}, [filters, filterData]);
|
||||||
|
|
||||||
|
if (!filterChips.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row my-2">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="d-flex flex-wrap align-items-start gap-2">
|
||||||
|
{filterChips.map((chip) => (
|
||||||
|
<div
|
||||||
|
key={chip.key}
|
||||||
|
className="d-flex align-items-center flex-wrap px-2 py-1"
|
||||||
|
style={{ fontSize: "0.9rem" }}
|
||||||
|
>
|
||||||
|
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||||
|
{chip.items.map((item) => (
|
||||||
|
<span
|
||||||
|
key={item.id}
|
||||||
|
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white btn-sm ms-2"
|
||||||
|
style={{
|
||||||
|
filter: "invert(1) grayscale(1)",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
}}
|
||||||
|
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoteFilterChips;
|
||||||
94
src/components/Documents/DocumentFilterChips.jsx
Normal file
94
src/components/Documents/DocumentFilterChips.jsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||||
|
// Normalize structure: handle both "filterData.data" and plain "filterData"
|
||||||
|
const data = filterData?.data || filterData || {};
|
||||||
|
|
||||||
|
const filterChips = useMemo(() => {
|
||||||
|
const chips = [];
|
||||||
|
|
||||||
|
const buildGroup = (ids, list, label, key) => {
|
||||||
|
if (!ids?.length) return;
|
||||||
|
const items = ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
name: list?.find((item) => item.id === id)?.name || id,
|
||||||
|
}));
|
||||||
|
chips.push({ key, label, items });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build chips using normalized data
|
||||||
|
buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds");
|
||||||
|
buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds");
|
||||||
|
buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds");
|
||||||
|
buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds");
|
||||||
|
|
||||||
|
if (filters.statusIds?.length) {
|
||||||
|
const items = filters.statusIds.map((status) => ({
|
||||||
|
id: status,
|
||||||
|
name:
|
||||||
|
status === true
|
||||||
|
? "Verified"
|
||||||
|
: status === false
|
||||||
|
? "Rejected"
|
||||||
|
: "Pending",
|
||||||
|
}));
|
||||||
|
chips.push({ key: "statusIds", label: "Status", items });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.startDate || filters.endDate) {
|
||||||
|
const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : "";
|
||||||
|
const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : "";
|
||||||
|
chips.push({
|
||||||
|
key: "dateRange",
|
||||||
|
label: "Date Range",
|
||||||
|
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chips;
|
||||||
|
}, [filters, filterData]);
|
||||||
|
|
||||||
|
if (!filterChips.length) return null;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row my-2">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="d-flex flex-wrap align-items-start gap-1">
|
||||||
|
{filterChips.map((chip) => (
|
||||||
|
<div
|
||||||
|
key={chip.key}
|
||||||
|
className="d-flex align-items-center flex-wrap px-2 py-1"
|
||||||
|
style={{ fontSize: "0.9rem" }}
|
||||||
|
>
|
||||||
|
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||||
|
{chip.items.map((item) => (
|
||||||
|
<span
|
||||||
|
key={item.id}
|
||||||
|
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white btn-sm ms-2"
|
||||||
|
style={{
|
||||||
|
filter: "invert(1) grayscale(1)",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
}}
|
||||||
|
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentFilterChips;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react";
|
||||||
import { useDocumentFilterEntities } from "../../hooks/useDocument";
|
import { useDocumentFilterEntities } from "../../hooks/useDocument";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -9,16 +9,34 @@ import {
|
|||||||
import { DateRangePicker1 } from "../common/DateRangePicker";
|
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
const DocumentFilterPanel = forwardRef(
|
||||||
|
({ entityTypeId, onApply, setFilterdata }, ref) => {
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
const { status } = useParams();
|
||||||
|
|
||||||
const { data, isError, isLoading, error } =
|
const { data, isError, isLoading, error } =
|
||||||
useDocumentFilterEntities(entityTypeId);
|
useDocumentFilterEntities(entityTypeId);
|
||||||
|
|
||||||
|
//changes
|
||||||
|
|
||||||
|
const dynamicDocumentFilterDefaultValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...DocumentFilterDefaultValues,
|
||||||
|
uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [],
|
||||||
|
documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [],
|
||||||
|
documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [],
|
||||||
|
documentTagIds: DocumentFilterDefaultValues.documentTagIds || [],
|
||||||
|
startDate: DocumentFilterDefaultValues.startDate,
|
||||||
|
endDate: DocumentFilterDefaultValues.endDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(DocumentFilterSchema),
|
resolver: zodResolver(DocumentFilterSchema),
|
||||||
defaultValues: DocumentFilterDefaultValues,
|
defaultValues: dynamicDocumentFilterDefaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, reset, setValue, watch } = methods;
|
const { handleSubmit, reset, setValue, watch } = methods;
|
||||||
@ -32,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
resetFieldValue: (name, value) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getValues: methods.getValues, // optional, to read current filter state
|
||||||
|
}));
|
||||||
|
|
||||||
|
//changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && setFilterdata) {
|
||||||
|
setFilterdata(data);
|
||||||
|
}
|
||||||
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
const onSubmit = (values) => {
|
const onSubmit = (values) => {
|
||||||
onApply({
|
onApply({
|
||||||
...values,
|
...values,
|
||||||
@ -42,14 +78,14 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
? moment.utc(values.endDate, "DD-MM-YYYY").toISOString()
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
closePanel();
|
// closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
reset(DocumentFilterDefaultValues);
|
reset(DocumentFilterDefaultValues);
|
||||||
setResetKey((prev) => prev + 1);
|
setResetKey((prev) => prev + 1);
|
||||||
onApply(DocumentFilterDefaultValues);
|
onApply(DocumentFilterDefaultValues);
|
||||||
closePanel();
|
// closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <div>Loading...</div>;
|
||||||
@ -63,6 +99,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
documentTag = [],
|
documentTag = [],
|
||||||
} = data?.data || {};
|
} = data?.data || {};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
@ -73,18 +111,16 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${isUploadedAt ? "active btn-secondary text-white" : ""
|
||||||
isUploadedAt ? "active btn-secondary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isUploadedAt", true)}
|
onClick={() => setValue("isUploadedAt", true)}
|
||||||
>
|
>
|
||||||
Uploaded On
|
Uploaded On
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${!isUploadedAt ? "active btn-secondary text-white" : ""
|
||||||
!isUploadedAt ? "active btn-secondary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isUploadedAt", false)}
|
onClick={() => setValue("isUploadedAt", false)}
|
||||||
>
|
>
|
||||||
Updated On
|
Updated On
|
||||||
@ -189,18 +225,18 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-label-secondary btn-xs"
|
className="btn btn-label-secondary btn-sm"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary btn-xs">
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default DocumentFilterPanel;
|
export default DocumentFilterPanel;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import NewDocument from "./ManageDocument";
|
import NewDocument from "./ManageDocument";
|
||||||
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants";
|
||||||
@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument";
|
|||||||
import DocumentViewerModal from "./DocumentViewerModal";
|
import DocumentViewerModal from "./DocumentViewerModal";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
import DocumentFilterChips from "./DocumentFilterChips";
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
export const DocumentContext = createContext();
|
export const DocumentContext = createContext();
|
||||||
@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
const [isSelf, setIsSelf] = useState(false);
|
const [isSelf, setIsSelf] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [isActive, setIsActive] = useState(true);
|
const [isActive, setIsActive] = useState(true);
|
||||||
const [filters, setFilter] = useState();
|
const [filters, setFilter] = useState(DocumentFilterDefaultValues);
|
||||||
const [isRefetching, setIsRefetching] = useState(false);
|
const [isRefetching, setIsRefetching] = useState(false);
|
||||||
const [refetchFn, setRefetchFn] = useState(null);
|
const [refetchFn, setRefetchFn] = useState(null);
|
||||||
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
const [DocumentEntity, setDocumentEntity] = useState(Document_Entity);
|
||||||
const { employeeId } = useParams();
|
const { employeeId } = useParams();
|
||||||
const [OpenDocument, setOpenDocument] = useState(false);
|
const [OpenDocument, setOpenDocument] = useState(false);
|
||||||
|
const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues);
|
||||||
|
const updatedRef = useRef();
|
||||||
const [ManageDoc, setManageDoc] = useState({
|
const [ManageDoc, setManageDoc] = useState({
|
||||||
document: null,
|
document: null,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
setShowTrigger(true);
|
setShowTrigger(true);
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Document Filters",
|
"Document Filters",
|
||||||
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} />
|
<DocumentFilterPanel entityTypeId={DocumentEntity} onApply={setFilter} setFilterdata={setFilterdata} ref={updatedRef} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
setDocumentEntity(Document_Entity);
|
setDocumentEntity(Document_Entity);
|
||||||
}
|
}
|
||||||
}, [Document_Entity]);
|
}, [Document_Entity]);
|
||||||
|
|
||||||
|
|
||||||
|
const removeFilterChip = (key, id) => {
|
||||||
|
const updatedFilters = { ...filters };
|
||||||
|
if (Array.isArray(updatedFilters[key])) {
|
||||||
|
updatedFilters[key] = updatedFilters[key].filter((v) => v !== id);
|
||||||
|
updatedRef.current?.resetFieldValue(key,updatedFilters[key]);
|
||||||
|
}
|
||||||
|
else if (key === "dateRange") {
|
||||||
|
updatedFilters.startDate = null;
|
||||||
|
updatedFilters.endDate = null;
|
||||||
|
updatedRef.current?.resetFieldValue("startDate",null);
|
||||||
|
updatedRef.current?.resetFieldValue("endDate",null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updatedFilters[key] = null;
|
||||||
|
}
|
||||||
|
setFilter(updatedFilters);
|
||||||
|
return updatedFilters;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentContext.Provider value={contextValues}>
|
<DocumentContext.Provider value={contextValues}>
|
||||||
<div className="mt-5">
|
<div className="mt-2">
|
||||||
<div className="card page-min-h d-flex p-2">
|
<div className="card page-min-h d-flex p-5">
|
||||||
|
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
<div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
{" "}
|
{" "}
|
||||||
<input
|
<input
|
||||||
@ -149,7 +174,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-6 col-md-6 col-lg-8 text-end">
|
<div className="col-12 col-md-6 col-lg-8 text-end">
|
||||||
{(isSelf || canUploadDocument) && (
|
{(isSelf || canUploadDocument) && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary me-3"
|
className="btn btn-sm btn-primary me-3"
|
||||||
@ -231,4 +256,4 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Documents;
|
export default Documents;
|
||||||
@ -82,9 +82,9 @@ const DocumentsList = ({
|
|||||||
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
if (isLoading || isFetching) return <DocumentTableSkeleton />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
return <div>Error: {error?.message || "Something went wrong"}</div>;
|
||||||
if (isInitialEmpty) return <div>No documents found yet.</div>;
|
if (isInitialEmpty) return <div className="py-12 my-12">No documents found yet.</div>;
|
||||||
if (isSearchEmpty) return <div>No results found for "{debouncedSearch}"</div>;
|
if (isSearchEmpty) return <div className="py-12 my-12">No results found for "{debouncedSearch}"</div>;
|
||||||
if (isFilterEmpty) return <div>No documents match your filter.</div>;
|
if (isFilterEmpty) return <div className="py-12 my-12">No documents match your filter.</div>;
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
ActiveInActive(
|
ActiveInActive(
|
||||||
@ -138,16 +138,14 @@ const DocumentsList = ({
|
|||||||
lastName={e.uploadedBy?.lastName}
|
lastName={e.uploadedBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-truncate ms-1">
|
<span className="text-truncate ms-1">
|
||||||
{`${e.uploadedBy?.firstName ?? ""} ${
|
{`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
|
||||||
e.uploadedBy?.lastName ?? ""
|
}`.trim() || "N/A"}
|
||||||
}`.trim() || "N/A"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
getValue: (e) =>
|
getValue: (e) =>
|
||||||
`${e.uploadedBy?.firstName ?? ""} ${
|
`${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? ""
|
||||||
e.uploadedBy?.lastName ?? ""
|
}`.trim() || "N/A",
|
||||||
}`.trim() || "N/A",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "uploadedAt",
|
key: "uploadedAt",
|
||||||
@ -217,7 +215,7 @@ const DocumentsList = ({
|
|||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
{(isSelf || canModifyDocument) && (
|
{(isSelf || canModifyDocument) && (
|
||||||
<i
|
<i
|
||||||
className="bx bx-edit text-secondary cursor-pointer"
|
className="bx bx-edit text-secondary cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -226,7 +224,7 @@ const DocumentsList = ({
|
|||||||
></i>
|
></i>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isSelf || canDeleteDocument) && (
|
{(isSelf || canDeleteDocument) && (
|
||||||
<i
|
<i
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
className="bx bx-trash text-danger cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -51,7 +51,6 @@ const EmpAttendance = () => {
|
|||||||
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
|
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(sorted);
|
|
||||||
|
|
||||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
||||||
sorted,
|
sorted,
|
||||||
|
|||||||
84
src/components/Employee/handleEmployeeExport.jsx
Normal file
84
src/components/Employee/handleEmployeeExport.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import { exportToExcel, exportToCSV, exportToPDF, printTable } from "../../utils/tableExportUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles export operations for employee data.
|
||||||
|
* @param {string} type - Export type: 'csv', 'excel', 'pdf', or 'print'
|
||||||
|
* @param {Array} employeeList - Full employee data array
|
||||||
|
* @param {Array} filteredData - Filtered employee data (if search applied)
|
||||||
|
* @param {string} searchText - Current search text (used to decide dataset)
|
||||||
|
* @param {RefObject} tableRef - Table reference (used for print)
|
||||||
|
*/
|
||||||
|
const handleEmployeeExport = (type, employeeList, filteredData, searchText, tableRef) => {
|
||||||
|
// Export full list (filtered if search applied)
|
||||||
|
const dataToExport = searchText ? filteredData : employeeList;
|
||||||
|
|
||||||
|
if (!dataToExport || dataToExport.length === 0) return;
|
||||||
|
|
||||||
|
// Map and format employee data for export
|
||||||
|
const exportData = dataToExport.map((item) => ({
|
||||||
|
"First Name": item.firstName || "",
|
||||||
|
"Middle Name": item.middleName || "",
|
||||||
|
"Last Name": item.lastName || "",
|
||||||
|
"Email": item.email || "",
|
||||||
|
"Gender": item.gender || "",
|
||||||
|
"Birth Date": item.birthdate
|
||||||
|
? moment(item.birthdate).format("DD-MMM-YYYY")
|
||||||
|
: "",
|
||||||
|
"Joining Date": item.joiningDate
|
||||||
|
? moment(item.joiningDate).format("DD-MMM-YYYY")
|
||||||
|
: "",
|
||||||
|
"Permanent Address": item.permanentAddress || "",
|
||||||
|
"Current Address": item.currentAddress || "",
|
||||||
|
"Phone Number": item.phoneNumber || "",
|
||||||
|
"Emergency Phone Number": item.emergencyPhoneNumber || "",
|
||||||
|
"Emergency Contact Person": item.emergencyContactPerson || "",
|
||||||
|
"Is Active": item.isActive ? "Active" : "Inactive",
|
||||||
|
"Job Role": item.jobRole || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "csv":
|
||||||
|
exportToCSV(exportData, "employees");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "excel":
|
||||||
|
exportToExcel(exportData, "employees");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "pdf":
|
||||||
|
exportToPDF(
|
||||||
|
dataToExport.map((item) => ({
|
||||||
|
Name: `${item.firstName || ""} ${item.lastName || ""}`.trim(),
|
||||||
|
Email: item.email || "",
|
||||||
|
"Phone Number": item.phoneNumber || "",
|
||||||
|
"Job Role": item.jobRole || "",
|
||||||
|
"Joining Date": item.joiningDate
|
||||||
|
? moment(item.joiningDate).format("DD-MMM-YYYY")
|
||||||
|
: "",
|
||||||
|
Gender: item.gender || "",
|
||||||
|
Status: item.isActive ? "Active" : "Inactive",
|
||||||
|
})),
|
||||||
|
"employees",
|
||||||
|
[
|
||||||
|
"Name",
|
||||||
|
"Email",
|
||||||
|
"Phone Number",
|
||||||
|
"Job Role",
|
||||||
|
"Joining Date",
|
||||||
|
"Gender",
|
||||||
|
"Status",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "print":
|
||||||
|
printTable(tableRef.current);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleEmployeeExport;
|
||||||
86
src/components/Expenses/ExpenseFilterChips.jsx
Normal file
86
src/components/Expenses/ExpenseFilterChips.jsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => {
|
||||||
|
// Build chips from filters
|
||||||
|
const filterChips = useMemo(() => {
|
||||||
|
const chips = [];
|
||||||
|
|
||||||
|
const buildGroup = (ids, list, label, key) => {
|
||||||
|
if (!ids?.length) return;
|
||||||
|
const items = ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
name: list.find((item) => item.id === id)?.name || id,
|
||||||
|
}));
|
||||||
|
chips.push({ key, label, items });
|
||||||
|
};
|
||||||
|
|
||||||
|
buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds");
|
||||||
|
buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds");
|
||||||
|
buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById");
|
||||||
|
buildGroup(filters.statusIds, filterData.status, "Status", "statusIds");
|
||||||
|
buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds");
|
||||||
|
|
||||||
|
if (filters.startDate || filters.endDate) {
|
||||||
|
const start = filters.startDate
|
||||||
|
? new Date(filters.startDate).toLocaleDateString()
|
||||||
|
: "";
|
||||||
|
const end = filters.endDate
|
||||||
|
? new Date(filters.endDate).toLocaleDateString()
|
||||||
|
: "";
|
||||||
|
chips.push({
|
||||||
|
key: "dateRange",
|
||||||
|
label: "Date Range",
|
||||||
|
items: [{ id: "dateRange", name: `${start} - ${end}` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chips;
|
||||||
|
}, [filters, filterData]);
|
||||||
|
|
||||||
|
if (!filterChips.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
|
||||||
|
{filterChips.map((chip) => (
|
||||||
|
<div
|
||||||
|
key={chip.key}
|
||||||
|
className="d-flex align-items-center flex-wrap px-2 py-1 "
|
||||||
|
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
|
||||||
|
>
|
||||||
|
{/* Chip Label */}
|
||||||
|
<span className="fw-semibold me-2">{chip.label}:</span>
|
||||||
|
|
||||||
|
{/* Chip Items */}
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||||
|
{chip.items.map((item) => (
|
||||||
|
<span
|
||||||
|
key={item.id}
|
||||||
|
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white btn-sm ms-2"
|
||||||
|
style={{
|
||||||
|
filter: "invert(1) grayscale(1)",
|
||||||
|
opacity: 0.7,
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
}}
|
||||||
|
onClick={() => removeFilterChip(chip.key, item.id)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseFilterChips;
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react";
|
||||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||||
@ -13,9 +13,11 @@ import { useSelector } from "react-redux";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useExpenseFilter } from "../../hooks/useExpense";
|
import { useExpenseFilter } from "../../hooks/useExpense";
|
||||||
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => {
|
||||||
|
const { status } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
(store) => store.localVariables.projectId
|
(store) => store.localVariables.projectId
|
||||||
);
|
);
|
||||||
@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
{ id: "submittedBy", name: "Submitted By" },
|
{ id: "submittedBy", name: "Submitted By" },
|
||||||
{ id: "project", name: "Project" },
|
{ id: "project", name: "Project" },
|
||||||
{ id: "paymentMode", name: "Payment Mode" },
|
{ id: "paymentMode", name: "Payment Mode" },
|
||||||
{ id: "expensesType", name: "Expense Type" },
|
{ id: "expensesType", name: "Expense Category" },
|
||||||
{ id: "createdAt", name: "Submitted Date" },
|
{ id: "createdAt", name: "Submitted Date" },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
|
const dynamicDefaultFilter = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...defaultFilter,
|
||||||
|
statusIds: status ? [status] : defaultFilter.statusIds || [],
|
||||||
|
projectIds: defaultFilter.projectIds || [],
|
||||||
|
createdByIds: defaultFilter.createdByIds || [],
|
||||||
|
paidById: defaultFilter.paidById || [],
|
||||||
|
ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [],
|
||||||
|
isTransactionDate: defaultFilter.isTransactionDate ?? true,
|
||||||
|
startDate: defaultFilter.startDate,
|
||||||
|
endDate: defaultFilter.endDate,
|
||||||
|
};
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(SearchSchema),
|
resolver: zodResolver(SearchSchema),
|
||||||
defaultValues: defaultFilter,
|
defaultValues: dynamicDefaultFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||||
@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Change here
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && setFilterdata) {
|
||||||
|
setFilterdata(data);
|
||||||
|
}
|
||||||
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
const handleGroupChange = (e) => {
|
const handleGroupChange = (e) => {
|
||||||
const group = groupByList.find((g) => g.id === e.target.value);
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
if (group) setSelectedGroup(group);
|
if (group) setSelectedGroup(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
resetFieldValue: (name, value) => {
|
||||||
|
// Reset specific field
|
||||||
|
if (value !== undefined) {
|
||||||
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getValues: methods.getValues, // optional, to read current filter state
|
||||||
|
}));
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
onApply({
|
onApply({
|
||||||
...formData,
|
...formData,
|
||||||
@ -71,17 +106,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
onApply(defaultFilter);
|
onApply(defaultFilter);
|
||||||
handleGroupBy(groupByList[0].id);
|
handleGroupBy(groupByList[0].id);
|
||||||
closePanel();
|
closePanel();
|
||||||
|
if (status) {
|
||||||
|
navigate("/expenses", { replace: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close popup when navigating to another component
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
closePanel();
|
closePanel();
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
|
const [appliedStatusId, setAppliedStatusId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!status || !data) return;
|
||||||
|
|
||||||
|
if (status !== appliedStatusId) {
|
||||||
|
const filterWithStatus = {
|
||||||
|
...dynamicDefaultFilter,
|
||||||
|
projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [],
|
||||||
|
startDate: dynamicDefaultFilter.startDate
|
||||||
|
? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString()
|
||||||
|
: undefined,
|
||||||
|
endDate: dynamicDefaultFilter.endDate
|
||||||
|
? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString()
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
onApply(filterWithStatus);
|
||||||
|
handleGroupBy(selectedGroup.id);
|
||||||
|
setAppliedStatusId(status);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
dynamicDefaultFilter,
|
||||||
|
onApply,
|
||||||
|
handleGroupBy,
|
||||||
|
selectedGroup.id,
|
||||||
|
appliedStatusId,
|
||||||
|
selectedProjectId, // ✅ Added dependency
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||||
if (isError && isFetched)
|
if (isError && isFetched)
|
||||||
return <div>Something went wrong Here- {error.message} </div>;
|
return <div>Something went wrong Here- {error.message} </div>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
@ -92,18 +165,16 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
isTransactionDate ? "active btn-primary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isTransactionDate", true)}
|
onClick={() => setValue("isTransactionDate", true)}
|
||||||
>
|
>
|
||||||
Transaction Date
|
Transaction Date
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
!isTransactionDate ? "active btn-primary text-white" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setValue("isTransactionDate", false)}
|
onClick={() => setValue("isTransactionDate", false)}
|
||||||
>
|
>
|
||||||
Submitted Date
|
Submitted Date
|
||||||
@ -143,6 +214,13 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
labelKey={(item) => item.name}
|
labelKey={(item) => item.name}
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
/>
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="ExpenseTypeIds"
|
||||||
|
label="Category :"
|
||||||
|
options={data.expensesType}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Status :</label>
|
<label className="form-label">Status :</label>
|
||||||
@ -214,6 +292,6 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ExpenseFilterPanel;
|
export default ExpenseFilterPanel;
|
||||||
@ -10,20 +10,29 @@ import {
|
|||||||
EXPENSE_REJECTEDBY,
|
EXPENSE_REJECTEDBY,
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
import { getColorNameFromHex, useDebounce } from "../../utils/appUtils";
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
getColorNameFromHex,
|
||||||
|
useDebounce,
|
||||||
|
} from "../../utils/appUtils";
|
||||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
import ConfirmModal from "../common/ConfirmModal";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import ExpenseFilterChips from "./ExpenseFilterChips";
|
||||||
|
import { defaultFilter } from "./ExpenseSchema";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
||||||
const [deletingId, setDeletingId] = useState(null);
|
const [deletingId, setDeletingId] = useState(null);
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const { setViewExpense, setManageExpenseModal } = useExpenseContext();
|
const { setViewExpense, setManageExpenseModal, filterData, removeFilterChip } = useExpenseContext();
|
||||||
const IsExpenseEditable = useHasUserPermission();
|
const IsExpenseEditable = useHasUserPermission();
|
||||||
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const debouncedSearch = useDebounce(searchText, 500);
|
const debouncedSearch = useDebounce(searchText, 500);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
|
||||||
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
|
||||||
@ -59,40 +68,60 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
const groupByField = (items, field) => {
|
const groupByField = (items, field) => {
|
||||||
return items.reduce((acc, item) => {
|
return items.reduce((acc, item) => {
|
||||||
let key;
|
let key;
|
||||||
|
let displayField;
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case "transactionDate":
|
case "transactionDate":
|
||||||
key = item.transactionDate?.split("T")[0];
|
key = item?.transactionDate?.split("T")[0];
|
||||||
|
displayField = "Transaction Date";
|
||||||
break;
|
break;
|
||||||
case "status":
|
case "status":
|
||||||
key = item.status?.displayName || "Unknown";
|
key = item?.status?.displayName || "Unknown";
|
||||||
|
displayField = "Status";
|
||||||
break;
|
break;
|
||||||
case "submittedBy":
|
case "submittedBy":
|
||||||
key = `${item.createdBy?.firstName ?? ""} ${
|
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
||||||
item.createdBy?.lastName ?? ""
|
}`.trim();
|
||||||
}`.trim();
|
displayField = "Submitted By";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
key = item.project?.name || "Unknown Project";
|
key = item?.project?.name || "Unknown Project";
|
||||||
|
displayField = "Project";
|
||||||
break;
|
break;
|
||||||
case "paymentMode":
|
case "paymentMode":
|
||||||
key = item.paymentMode?.name || "Unknown Mode";
|
key = item?.paymentMode?.name || "Unknown Mode";
|
||||||
|
displayField = "Payment Mode";
|
||||||
break;
|
break;
|
||||||
case "expensesType":
|
case "expensesType":
|
||||||
key = item.expensesType?.name || "Unknown Type";
|
key = item?.expensesType?.name || "Unknown Type";
|
||||||
|
displayField = "Expense Category";
|
||||||
break;
|
break;
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
key = item.createdAt?.split("T")[0] || "Unknown Type";
|
key = item?.createdAt?.split("T")[0] || "Unknown Date";
|
||||||
|
displayField = "Created Date";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
key = "Others";
|
key = "Others";
|
||||||
|
displayField = "Others";
|
||||||
}
|
}
|
||||||
if (!acc[key]) acc[key] = [];
|
|
||||||
acc[key].push(item);
|
const groupKey = `${field}_${key}`; // unique key for object property
|
||||||
|
if (!acc[groupKey]) {
|
||||||
|
acc[groupKey] = { key, displayField, items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[groupKey].items.push(item);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expenseColumns = [
|
const expenseColumns = [
|
||||||
|
{
|
||||||
|
key: "expenseUId",
|
||||||
|
label: "Expense Id",
|
||||||
|
getValue: (e) => e.expenseUId || "N/A",
|
||||||
|
align: "text-start mx-2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "expensesType",
|
key: "expensesType",
|
||||||
label: "Expense Type",
|
label: "Expense Type",
|
||||||
@ -110,11 +139,11 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
label: "Submitted By",
|
label: "Submitted By",
|
||||||
align: "text-start",
|
align: "text-start",
|
||||||
getValue: (e) =>
|
getValue: (e) =>
|
||||||
`${e.createdBy?.firstName ?? ""} ${
|
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||||
e.createdBy?.lastName ?? ""
|
}`.trim() || "N/A",
|
||||||
}`.trim() || "N/A",
|
|
||||||
customRender: (e) => (
|
customRender: (e) => (
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center cursor-pointer"
|
||||||
|
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
classAvatar="m-0"
|
classAvatar="m-0"
|
||||||
@ -122,9 +151,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
lastName={e.createdBy?.lastName}
|
lastName={e.createdBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-truncate">
|
<span className="text-truncate">
|
||||||
{`${e.createdBy?.firstName ?? ""} ${
|
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
|
||||||
e.createdBy?.lastName ?? ""
|
}`.trim() || "N/A"}
|
||||||
}`.trim() || "N/A"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -138,11 +166,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
label: "Amount",
|
label: "Amount",
|
||||||
getValue: (e) => (
|
getValue: (e) => <>{formatCurrency(e?.amount)}</>,
|
||||||
<>
|
|
||||||
<i className="bx bx-rupee b-xs"></i> {e?.amount}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
isAlwaysVisible: true,
|
isAlwaysVisible: true,
|
||||||
align: "text-end",
|
align: "text-end",
|
||||||
},
|
},
|
||||||
@ -152,9 +176,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
align: "text-center",
|
align: "text-center",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${
|
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
|
||||||
getColorNameFromHex(e?.status?.color) || "secondary"
|
}`}
|
||||||
}`}
|
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{e.status?.name || "Unknown"}
|
{e.status?.name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
@ -162,27 +187,30 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isInitialLoading) return <ExpenseTableSkeleton />;
|
if (isInitialLoading && !data) return <ExpenseTableSkeleton />;
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error?.message}</div>;
|
||||||
|
|
||||||
const grouped = groupBy
|
const grouped = groupBy
|
||||||
? groupByField(data?.data ?? [], groupBy)
|
? groupByField(data?.data ?? [], groupBy)
|
||||||
: { All: data?.data ?? [] };
|
: { All: data?.data ?? [] };
|
||||||
const IsGroupedByDate = ["transactionDate", "createdAt"].includes(groupBy);
|
const IsGroupedByDate = [
|
||||||
|
{ key: "transactionDate", displayField: "Transaction Date" },
|
||||||
|
{ key: "createdAt", displayField: "created Date" },
|
||||||
|
]?.includes(groupBy);
|
||||||
|
|
||||||
const canEditExpense = (expense) => {
|
const canEditExpense = (expense) => {
|
||||||
return (
|
return (
|
||||||
(expense.status.id === EXPENSE_DRAFT ||
|
(expense?.status?.id === EXPENSE_DRAFT ||
|
||||||
EXPENSE_REJECTEDBY.includes(expense.status.id)) &&
|
EXPENSE_REJECTEDBY.includes(expense?.status?.id)) &&
|
||||||
expense.createdBy?.id === SelfId
|
expense?.createdBy?.id === SelfId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canDetetExpense = (expense) => {
|
const canDetetExpense = (expense) => {
|
||||||
return (
|
return (
|
||||||
expense.status.id === EXPENSE_DRAFT && expense.createdBy.id === SelfId
|
expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IsDeleteModalOpen && (
|
{IsDeleteModalOpen && (
|
||||||
@ -198,7 +226,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card px-0 px-sm-4">
|
<div className="card page-min-h px-sm-4">
|
||||||
|
{/* Filter Chips */}
|
||||||
|
<ExpenseFilterChips
|
||||||
|
filters={filters}
|
||||||
|
filterData={filterData}
|
||||||
|
removeFilterChip={removeFilterChip}
|
||||||
|
groupBy={groupBy}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive "
|
className="card-datatable table-responsive "
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
@ -226,18 +261,24 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.keys(grouped).length > 0 ? (
|
{Object.keys(grouped).length > 0 ? (
|
||||||
Object.entries(grouped).map(([group, expenses]) => (
|
Object.values(grouped).map(({ key, displayField, items }) => (
|
||||||
<React.Fragment key={group}>
|
<React.Fragment key={key}>
|
||||||
<tr className="tr-group text-dark">
|
<tr className="tr-group text-dark">
|
||||||
<td colSpan={8} className="text-start">
|
<td colSpan={8} className="text-start">
|
||||||
<strong>
|
<div className="d-flex align-items-center">
|
||||||
{IsGroupedByDate
|
{" "}
|
||||||
? formatUTCToLocalTime(group)
|
<small className="fs-6 py-1">
|
||||||
: group}
|
{displayField} :{" "}
|
||||||
</strong>
|
</small>{" "}
|
||||||
|
<small className="fs-6 ms-3">
|
||||||
|
{IsGroupedByDate
|
||||||
|
? formatUTCToLocalTime(key)
|
||||||
|
: key}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expenses.map((expense) => (
|
{items?.map((expense) => (
|
||||||
<tr key={expense.id}>
|
<tr key={expense.id}>
|
||||||
{expenseColumns.map(
|
{expenseColumns.map(
|
||||||
(col) =>
|
(col) =>
|
||||||
@ -263,27 +304,61 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
></i>
|
></i>
|
||||||
{canEditExpense(expense) && (
|
{canDetetExpense(expense) &&
|
||||||
<i
|
canEditExpense(expense) && (
|
||||||
className="bx bx-edit text-secondary cursor-pointer"
|
<div className="dropdown z-2">
|
||||||
onClick={() =>
|
<button
|
||||||
setManageExpenseModal({
|
type="button"
|
||||||
IsOpen: true,
|
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||||
expenseId: expense.id,
|
data-bs-toggle="dropdown"
|
||||||
})
|
aria-expanded="false"
|
||||||
}
|
>
|
||||||
></i>
|
<i
|
||||||
)}
|
className="bx bx-dots-vertical-rounded text-muted p-0"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-custom-class="tooltip-dark"
|
||||||
|
title="More Action"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||||
|
{canDetetExpense(expense) && (
|
||||||
|
<li
|
||||||
|
onClick={() =>
|
||||||
|
setManageExpenseModal({
|
||||||
|
IsOpen: true,
|
||||||
|
expenseId: expense.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
|
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||||
|
<span className="align-left ">
|
||||||
|
Modify
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
{canDetetExpense(expense) && (
|
{canDetetExpense(expense) && (
|
||||||
<i
|
<li
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
onClick={() => {
|
||||||
onClick={() => {
|
setIsDeleteModalOpen(true);
|
||||||
setIsDeleteModalOpen(true);
|
setDeletingId(expense.id);
|
||||||
setDeletingId(expense.id);
|
}}
|
||||||
}}
|
>
|
||||||
></i>
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
)}
|
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||||
|
<span className="align-left">
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -292,8 +367,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="text-center py-4">
|
<td colSpan={8} className="text-center border-0 ">
|
||||||
No Expense Found
|
<div className="py-8">
|
||||||
|
<p>No Expense Found</p>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,54 +1,137 @@
|
|||||||
import { useState } from "react";
|
import { useState, useRef ,useEffect} from "react";
|
||||||
|
|
||||||
const PreviewDocument = ({ imageUrl }) => {
|
const PreviewDocument = ({ imageUrl }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
// Zoom handlers
|
||||||
|
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.2, 3));
|
||||||
|
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.2, 0.5));
|
||||||
|
|
||||||
|
// Mouse wheel zoom
|
||||||
|
const handleWheel = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||||
|
setZoom((prev) => Math.min(Math.max(prev + delta, 0.5), 3));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("wheel", handleWheel);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (zoom <= 1) return;
|
||||||
|
setIsDragging(true);
|
||||||
|
setStartPos({
|
||||||
|
x: e.clientX - position.x,
|
||||||
|
y: e.clientY - position.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
setPosition({
|
||||||
|
x: e.clientX - startPos.x,
|
||||||
|
y: e.clientY - startPos.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => setIsDragging(false);
|
||||||
|
const handleMouseLeave = () => setIsDragging(false);
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setRotation(0);
|
||||||
|
setZoom(1);
|
||||||
|
setPosition({ x: 0, y: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-start">
|
<div className="d-flex justify-content-start align-items-center gap-3 mb-2 px-3 py-2 px-md-0 py-md-0">
|
||||||
<i
|
<i
|
||||||
className="bx bx-rotate-right cursor-pointer"
|
className="bx bx-rotate-right fs-4 cursor-pointer"
|
||||||
|
title="Rotate Right"
|
||||||
onClick={() => setRotation((prev) => prev + 90)}
|
onClick={() => setRotation((prev) => prev + 90)}
|
||||||
></i>
|
></i>
|
||||||
|
<i
|
||||||
|
className="bx bx-zoom-in fs-4 cursor-pointer"
|
||||||
|
title="Zoom In"
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
className="bx bx-zoom-out fs-4 cursor-pointer"
|
||||||
|
title="Zoom Out"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
className="bx bx-reset fs-4 cursor-pointer"
|
||||||
|
title="Reset"
|
||||||
|
onClick={handleReset}
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="d-flex flex-column justify-content-center align-items-center"
|
|
||||||
style={{ minHeight: "60%" }}
|
|
||||||
>
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<div className="text-secondary text-center mb-2">Loading...</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-3 d-flex justify-content-center align-items-center">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
className="d-flex justify-content-center align-items-center overflow-hidden border rounded "
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "80vh",
|
||||||
|
background: "#f8f9fa",
|
||||||
|
cursor: zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default",
|
||||||
|
userSelect: "none",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading && (
|
||||||
|
<div className="text-secondary text-center position-absolute">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Full View"
|
alt="Preview"
|
||||||
className="img-fluid"
|
|
||||||
style={{
|
|
||||||
maxHeight: "80vh",
|
|
||||||
objectFit: "contain",
|
|
||||||
display: loading ? "none" : "block",
|
|
||||||
transform: `rotate(${rotation}deg)`,
|
|
||||||
transition: "transform 0.3s ease",
|
|
||||||
}}
|
|
||||||
onLoad={() => setLoading(false)}
|
onLoad={() => setLoading(false)}
|
||||||
|
style={{
|
||||||
|
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${zoom})`,
|
||||||
|
transition: isDragging ? "none" : "transform 0.3s ease",
|
||||||
|
objectFit: "contain",
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
display: loading ? "none" : "block",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex justify-content-center gap-2">
|
{/* <div className="d-flex justify-content-center gap-2 mt-2">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-secondary"
|
className="btn btn-sm btn-outline-secondary"
|
||||||
onClick={() => setRotation(0)}
|
onClick={handleReset}
|
||||||
title="Reset Rotation"
|
title="Reset View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-reset"></i> Reset
|
<i className="bx bx-reset"></i> Reset View
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PreviewDocument;
|
export default PreviewDocument;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,11 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
||||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||||
import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils";
|
import {
|
||||||
|
getColorNameFromHex,
|
||||||
|
getIconByFileType,
|
||||||
|
localToUtc,
|
||||||
|
} from "../../utils/appUtils";
|
||||||
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import {
|
import {
|
||||||
@ -301,15 +305,15 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
|
|
||||||
<div className="d-flex flex-wrap gap-2">
|
<div className="d-flex flex-wrap gap-2">
|
||||||
{data?.documents?.map((doc) => {
|
{data?.documents?.map((doc) => {
|
||||||
const isImage = doc.contentType?.includes("image");
|
const isImage = doc.contentType?.startsWith("image");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={doc.documentId}
|
key={doc.documentId}
|
||||||
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
||||||
style={{
|
style={{
|
||||||
width: "80px",
|
width: "80px",
|
||||||
cursor: isImage ? "pointer" : "default",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@ -317,6 +321,8 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
IsOpen: true,
|
IsOpen: true,
|
||||||
Image: doc.preSignedUrl,
|
Image: doc.preSignedUrl,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
window.open(doc.preSignedUrl, "_blank");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -332,7 +338,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}) ?? "No Attachment"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -418,7 +424,9 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
|
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
|
||||||
(IsRejectedExpense && isCreatedBy)) && (
|
(IsRejectedExpense && isCreatedBy)) && (
|
||||||
<>
|
<>
|
||||||
<Label className="form-label me-2 mb-0" required>Comment</Label>
|
<Label className="form-label me-2 mb-0" required>
|
||||||
|
Comment
|
||||||
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register("comment")}
|
{...register("comment")}
|
||||||
@ -440,7 +448,7 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
key={status.id || index}
|
key={status.id || index}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setClickedStatusId(status.id);
|
setClickedStatusId(status.id);
|
||||||
setValue("statusId", status.id);
|
setValue("statusId", status.id);
|
||||||
handleSubmit(onSubmit)();
|
handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const Sidebar = () => {
|
|||||||
>
|
>
|
||||||
<div className="app-brand" style={{ paddingLeft: "30px" }}>
|
<div className="app-brand" style={{ paddingLeft: "30px" }}>
|
||||||
<Link to="/dashboard" className="app-brand-link">
|
<Link to="/dashboard" className="app-brand-link">
|
||||||
<span className="app-brand-logo rounded-circle app-brand-logo-border">
|
{/* <span className="app-brand-logo rounded-circle app-brand-logo-border">
|
||||||
<img
|
<img
|
||||||
className="app-brand-logo-sidebar"
|
className="app-brand-logo-sidebar"
|
||||||
src="/img/brand/marco.png"
|
src="/img/brand/marco.png"
|
||||||
@ -23,8 +23,19 @@ const Sidebar = () => {
|
|||||||
aria-label="logo image"
|
aria-label="logo image"
|
||||||
style={{ margin: "5px", paddingRight: "5px" }}
|
style={{ margin: "5px", paddingRight: "5px" }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> */}
|
||||||
<span className="app-brand-text menu-text fw-bold ms-2">PMS</span>
|
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="app-brand-link fw-bold navbar-brand text-green fs-6"
|
||||||
|
>
|
||||||
|
<span class="app-brand-logo demo">
|
||||||
|
<img src="/img/brand/marco.png" width="50" />
|
||||||
|
</span>
|
||||||
|
<span class="text-blue">OnField</span>
|
||||||
|
<span>Work</span>
|
||||||
|
<span class="text-dark">.com</span>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<a className="layout-menu-toggle menu-link text-large ms-auto">
|
<a className="layout-menu-toggle menu-link text-large ms-auto">
|
||||||
|
|||||||
@ -12,13 +12,12 @@ import { spridSchema } from "./OrganizationSchema";
|
|||||||
import { OrgCardSkeleton } from "./OrganizationSkeleton";
|
import { OrgCardSkeleton } from "./OrganizationSkeleton";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
// Zod schema: only allow exactly 4 digits
|
// Zod schema: only allow exactly 4 digits
|
||||||
|
|
||||||
const OrgPickerFromSPId = ({ title, placeholder }) => {
|
const OrgPickerFromSPId = ({ title, placeholder }) => {
|
||||||
const { onClose, startStep, flowType, onOpen, prevStep,orgData } =
|
const { onClose, startStep, flowType, onOpen, prevStep, orgData } =
|
||||||
useOrganizationModal();
|
useOrganizationModal();
|
||||||
const clientQuery = useQueryClient()
|
const clientQuery = useQueryClient();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -37,33 +36,42 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
|
|||||||
const onSubmit = (formdata) => {
|
const onSubmit = (formdata) => {
|
||||||
setSPRID(formdata.spridSearchText);
|
setSPRID(formdata.spridSearchText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCrateOrg = () => {
|
const handleCrateOrg = () => {
|
||||||
clientQuery.removeQueries({queryKey:["organization"]})
|
clientQuery.removeQueries({ queryKey: ["organization"] });
|
||||||
onOpen({ startStep: 4,orgData:null })
|
onOpen({ startStep: 4, orgData: null });
|
||||||
};
|
};
|
||||||
const SP = watch("spridSearchText");
|
const SP = watch("spridSearchText");
|
||||||
return (
|
return (
|
||||||
<div className="d-block">
|
<div className="d-block mt-4">
|
||||||
<form
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
className="d-flex flex-row gap-6 text-start align-items-center"
|
<div className="row align-items-center g-2">
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
{/* Input Section */}
|
||||||
>
|
<div className="col-12 col-md-8 d-block d-md-flex align-items-center gap-2 m-0 text-start">
|
||||||
<div className="d-flex flex-row align-items-center gap-2">
|
<Label className="text-nowrap mb-1 mb-md-0" required>
|
||||||
<Label className="text-secondary">Search by SPRID</Label>
|
Search by SPRID
|
||||||
<input
|
</Label>
|
||||||
type="search"
|
<input
|
||||||
{...register("spridSearchText")}
|
type="search"
|
||||||
className="form-control form-control-sm w-auto"
|
{...register("spridSearchText")}
|
||||||
placeholder="Enter SPRID"
|
className="form-control form-control-sm flex-grow-1"
|
||||||
maxLength={4}
|
placeholder="Enter SPRID"
|
||||||
/>
|
maxLength={4}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" className="btn btn-sm btn-primary">
|
{/* Button Section */}
|
||||||
<i className="bx bx-sm bx-search-alt-2"></i> Search
|
<div className="col-12 col-md-4 text-md-start text-center mt-2 mt-md-0">
|
||||||
</button>
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary w-100 w-md-auto"
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm bx-search-alt-2"></i> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="text-start danger-text">
|
<div className="text-start danger-text">
|
||||||
{" "}
|
{" "}
|
||||||
{errors.spridSearchText && (
|
{errors.spridSearchText && (
|
||||||
@ -124,7 +132,7 @@ const OrgPickerFromSPId = ({ title, placeholder }) => {
|
|||||||
No organization found for "{SPRID}"
|
No organization found for "{SPRID}"
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="py-12 text-center text-tiny text-black">
|
<div className="py-2 text-center text-tiny text-black">
|
||||||
<small className="d-block text-secondary">
|
<small className="d-block text-secondary">
|
||||||
Do not have SPRID or could not find organization ?
|
Do not have SPRID or could not find organization ?
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@ -93,10 +93,12 @@ const OrganizationsList = ({searchText}) => {
|
|||||||
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
|
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card px-0 px-sm-4 pb-12 pt-5">
|
<div
|
||||||
<div className="card-datatable table-responsive" id="horizontal-example">
|
className="card-datatable table-responsive overflow-auto"
|
||||||
<div className="dataTables_wrapper no-footer px-2">
|
id="horizontal-example"
|
||||||
<table className="table border-top dataTable text-nowrap">
|
>
|
||||||
|
<div className="dataTables_wrapper no-footer px-2 ">
|
||||||
|
<table className="table border-top dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="table_header_border">
|
<tr className="table_header_border">
|
||||||
{organizationsColumns.map((col) => (
|
{organizationsColumns.map((col) => (
|
||||||
@ -131,7 +133,7 @@ const OrganizationsList = ({searchText}) => {
|
|||||||
<div className="d-flex justify-content-center gap-2">
|
<div className="d-flex justify-content-center gap-2">
|
||||||
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
|
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
|
||||||
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
|
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
|
||||||
<i className="bx bx-trash text-danger cursor-pointer"></i>
|
<i className="bx bx-trash text-danger cursor-not-allowed"></i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -157,7 +159,6 @@ const OrganizationsList = ({searchText}) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -22,9 +22,8 @@ const VieworgDataanization = ({ orgId }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-end">
|
<div className="text-end">
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${
|
className={`badge bg-label-${data?.isActive ? "primary" : "secondary"
|
||||||
data?.isActive ? "primary" : "secondary"
|
} `}
|
||||||
} `}
|
|
||||||
>
|
>
|
||||||
{data?.isActive ? "Active" : "In-Active"}{" "}
|
{data?.isActive ? "Active" : "In-Active"}{" "}
|
||||||
</span>
|
</span>
|
||||||
@ -105,9 +104,101 @@ const VieworgDataanization = ({ orgId }) => {
|
|||||||
<div className="text-muted text-start">{data?.address}</div>
|
<div className="text-muted text-start">{data?.address}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex text-secondary mb-2">
|
<div className="col-12 mb-3">
|
||||||
{" "}
|
<div
|
||||||
<i className="bx bx-sm bx-briefcase me-1" /> Projects And Services
|
className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapse-projects-services"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i className="bx bx-sm bx-briefcase me-1" /> Projects
|
||||||
|
</div>
|
||||||
|
<i className="bx bx-chevron-down me-2"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* remove "show" from className */}
|
||||||
|
<div id="collapse-projects-services" className="collapse">
|
||||||
|
{data?.projects && data.projects.length > 0 ? (
|
||||||
|
data.projects
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
const projectId = curr.project.id;
|
||||||
|
if (!acc.find((p) => p.id === projectId)) {
|
||||||
|
acc.push(curr.project);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.map((project) => (
|
||||||
|
<div key={project.id} className="mb-2 rounded p-2">
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-between align-items-center cursor-pointer"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target={`#collapse-${project.id}`}
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<label className="form-label fw-semibold">
|
||||||
|
<i className="bx bx-buildings me-2"></i>
|
||||||
|
{project.name}
|
||||||
|
</label>
|
||||||
|
<i className="bx bx-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id={`collapse-${project.id}`} className="collapse mt-2 ps-5">
|
||||||
|
{data.projects
|
||||||
|
.filter((p) => p.project.id === project.id)
|
||||||
|
.map((p) => (
|
||||||
|
<div key={p.service.id} className="mb-1 text-muted">
|
||||||
|
<i className="bx bx-wrench me-2"></i>
|
||||||
|
{p.service.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-muted fst-italic ps-2">No projects available</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-between align-items-center text-secondary mb-2 cursor-pointer"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapse-services"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i className="bx bx-sm bx-cog me-1" /> Services
|
||||||
|
</div>
|
||||||
|
<i className="bx bx-chevron-down me-2"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* collapse is closed initially */}
|
||||||
|
<div id="collapse-services" className="collapse">
|
||||||
|
{data?.services && data.services.length > 0 ? (
|
||||||
|
<div className="row">
|
||||||
|
{data.services.map((service) => (
|
||||||
|
<div key={service.id} className="col-md-12 mb-3">
|
||||||
|
<div className="card h-100 shadow-sm border-0">
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="fw-semibold mb-1">
|
||||||
|
<i className="bx bx-wrench me-1"></i>
|
||||||
|
{service.name}
|
||||||
|
</h6>
|
||||||
|
<p className="text-muted small mb-0">
|
||||||
|
{service.description || "No description available."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted fst-italic ps-2">No services available</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-2">
|
<div className="col-6 col-md-2">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
completedWork={formatNumber(workArea?.completedWork)}
|
completedWork={formatNumber(workArea?.completedWork)}
|
||||||
plannedWork={formatNumber(workArea?.plannedWork)}
|
plannedWork={formatNumber(workArea?.plannedWork)}
|
||||||
|
|||||||
@ -40,31 +40,32 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
label: "Directory",
|
label: "Directory",
|
||||||
hidden: !(DirAdmin || DireManager || DirUser),
|
hidden: !(DirAdmin || DireManager || DirUser),
|
||||||
},
|
},
|
||||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
{ key: "documents", icon: "bx bx-folder-open", label: "Documents", hidden: !(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||||
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
{ key: "organization", icon: "bx bx-buildings", label: "Organization" },
|
||||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
{ key: "setting", icon: "bx bxs-cog", label: "Setting", hidden: !isManageTeam },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="nav-align-top">
|
<div className="table-responsive">
|
||||||
<ul className="nav nav-tabs">
|
<div className="nav-align-top">
|
||||||
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
|
<ul className="nav nav-tabs">
|
||||||
<li key={tab.key} className="nav-item cursor-pointer">
|
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
|
||||||
<a
|
<li key={tab.key} className="nav-item cursor-pointer">
|
||||||
|
<a
|
||||||
className={`nav-link ${
|
|
||||||
activePill === tab.key ? "active cursor-pointer" : ""
|
className={`nav-link ${activePill === tab.key ? "active cursor-pointer" : ""
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onPillClick(tab.key);
|
onPillClick(tab.key);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className={`${tab.icon} bx-sm me-1_5`}></i>
|
<i className={`${tab.icon} bx-sm me-1_5`}></i>
|
||||||
<span className="d-none d-md-inline ">{tab.label}</span>
|
<span className="d-none d-md-inline ">{tab.label}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -201,7 +201,7 @@ const Teams = () => {
|
|||||||
className="form-check-label ms-2"
|
className="form-check-label ms-2"
|
||||||
htmlFor="activeEmployeeSwitch"
|
htmlFor="activeEmployeeSwitch"
|
||||||
>
|
>
|
||||||
{activeEmployee ? "Active Employees" : "Include Inactive Employees"}
|
{activeEmployee ? "Active Employees" : "In-active Employees"}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const TenantsList = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-2 mt-3">
|
<div className=" mt-3">
|
||||||
<div className=" text-nowrap table-responsive">
|
<div className=" text-nowrap table-responsive">
|
||||||
<table className="table border-top dataTable text-nowrap">
|
<table className="table border-top dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -151,7 +151,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
if (isError) return <p>{error.message}</p>;
|
if (isError) return <p>{error.message}</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card ">
|
<div className="card px-sm-4 px-0">
|
||||||
<div
|
<div
|
||||||
className="card-datatable table-responsive page-min-h"
|
className="card-datatable table-responsive page-min-h"
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
|
|||||||
@ -172,15 +172,15 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
|
|
||||||
<div className="d-flex flex-wrap gap-2">
|
<div className="d-flex flex-wrap gap-2">
|
||||||
{data?.attachments?.map((doc) => {
|
{data?.attachments?.map((doc) => {
|
||||||
const isImage = doc.contentType?.includes("image");
|
const isImage = doc.contentType?.startsWith("image");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={doc.documentId}
|
key={doc.documentId}
|
||||||
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
||||||
style={{
|
style={{
|
||||||
width: "80px",
|
width: "80px",
|
||||||
cursor: isImage ? "pointer" : "default",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@ -188,6 +188,8 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
IsOpen: true,
|
IsOpen: true,
|
||||||
Image: doc.preSignedUrl,
|
Image: doc.preSignedUrl,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
window.open(doc.preSignedUrl, "_blank");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -73,7 +73,7 @@ const DateRangePicker = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<i
|
<i
|
||||||
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2 "
|
className="bx bx-calendar calendar-icon cursor-pointer position-absolute ms-n6 top-50 end-30 translate-middle-y me-2 "
|
||||||
onClick={handleIconClick}
|
onClick={handleIconClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,13 +37,13 @@ const GalleryFilterPanel = ({ onApply }) => {
|
|||||||
startDate: localToUtc(formData.startDate),
|
startDate: localToUtc(formData.startDate),
|
||||||
endDate: localToUtc(formData.endDate),
|
endDate: localToUtc(formData.endDate),
|
||||||
});
|
});
|
||||||
closePanel()
|
// closePanel()
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClear=()=>{
|
const onClear=()=>{
|
||||||
reset(defaultGalleryFilterValue);
|
reset(defaultGalleryFilterValue);
|
||||||
setResetKey((prev) => prev + 1);
|
setResetKey((prev) => prev + 1);
|
||||||
closePanel()
|
// closePanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) return <div>Loading....</div>;
|
if (isLoading) return <div>Loading....</div>;
|
||||||
|
|||||||
@ -28,13 +28,21 @@ const ImageGalleryListView = ({filter}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data?.data?.length && !isLoading) {
|
if (!data?.data?.length && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<p className="text-center text-muted mt-5">
|
<div
|
||||||
{selectedProject ? " No images match the selected filters.":"Please Select Project!"}
|
className="d-flex justify-content-center align-items-center text-muted"
|
||||||
</p>
|
style={{ minHeight: "50vh" }}
|
||||||
);
|
>
|
||||||
}
|
<span style={{ fontSize: "0.9rem" }}>
|
||||||
|
{selectedProject
|
||||||
|
? "No images match the selected filters."
|
||||||
|
: "Please Select Project!"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
|||||||
const ViewGallery = ({ batch, index }) => {
|
const ViewGallery = ({ batch, index }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentIndex, setCurrentIndex] = useState(index);
|
const [currentIndex, setCurrentIndex] = useState(index);
|
||||||
console.log(batch);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
}, [index, batch]);
|
}, [index, batch]);
|
||||||
|
|||||||
@ -200,35 +200,35 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export const useDashboard_AttendanceData = (date,projectId)=>{
|
export const useDashboard_AttendanceData = (date, projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["dashboardAttendances",date,projectId],
|
queryKey: ["dashboardAttendances", date, projectId],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
|
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDashboardTeamsCardData =(projectId)=>{
|
export const useDashboardTeamsCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["dashboardTeams",projectId],
|
queryKey: ["dashboardTeams", projectId],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
|
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDashboardTasksCardData = (projectId) => {
|
export const useDashboardTasksCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["dashboardTasks",projectId],
|
queryKey: ["dashboardTasks", projectId],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
|
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -236,7 +236,7 @@ export const useDashboardTasksCardData = (projectId) => {
|
|||||||
// return useQuery({
|
// return useQuery({
|
||||||
// queryKey:["dashboardAttendanceOverView",projectId],
|
// queryKey:["dashboardAttendanceOverView",projectId],
|
||||||
// queryFn:async()=> {
|
// queryFn:async()=> {
|
||||||
|
|
||||||
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
|
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
|
||||||
// return resp.data;
|
// return resp.data;
|
||||||
// }
|
// }
|
||||||
@ -244,12 +244,53 @@ export const useDashboardTasksCardData = (projectId) => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export const useDashboardProjectsCardData = () => {
|
export const useDashboardProjectsCardData = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["dashboardProjects"],
|
queryKey: ["dashboardProjects"],
|
||||||
queryFn:async()=> {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
|
||||||
|
const hasBothDates = !!startDate && !!endDate;
|
||||||
|
const noDatesSelected = !startDate && !endDate;
|
||||||
|
|
||||||
|
const shouldFetch =
|
||||||
|
noDatesSelected ||
|
||||||
|
hasBothDates;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: shouldFetch,
|
||||||
|
refetchOnWindowFocus: true, // refetch when you come back
|
||||||
|
refetchOnMount: "always", // always refetch on remount
|
||||||
|
staleTime: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExpenseStatus = (projectId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["expense_stauts", projectId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await GlobalRepository.getExpenseStatus(projectId);
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["expenseByProject", projectId, categoryId, months],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -23,13 +23,13 @@ const AuthLayout = () => {
|
|||||||
to="/"
|
to="/"
|
||||||
className="app-brand-link gap-2 position-fixed top-2 start-0 mx-2 mx-sm-4"
|
className="app-brand-link gap-2 position-fixed top-2 start-0 mx-2 mx-sm-4"
|
||||||
>
|
>
|
||||||
<span className="app-brand-logo rounded-circle ">
|
{/* <span className="app-brand-logo rounded-circle ">
|
||||||
<img
|
<img
|
||||||
src="/img/brand/marco.png"
|
src="/img/brand/marco.png"
|
||||||
alt="marco-logo"
|
alt="marco-logo"
|
||||||
className="app-brand-logo-login"
|
className="app-brand-logo-login"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> */}
|
||||||
</Link>
|
</Link>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -179,44 +179,40 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search + Organization filter */}
|
{/* Search + Organization filter */}
|
||||||
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center">
|
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto">
|
||||||
{/* Organization Dropdown */}
|
<div className="row g-2">
|
||||||
<div className="row">
|
<div className="col-12 col-sm-6">
|
||||||
<div className="col-12 col-sm-6 mb-2 mb-sm-0">
|
<select
|
||||||
<select
|
className="form-select form-select-sm"
|
||||||
className="form-select form-select-sm"
|
value={appliedFilters.selectedOrganization}
|
||||||
value={appliedFilters.selectedOrganization}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setAppliedFilters((prev) => ({
|
||||||
setAppliedFilters((prev) => ({
|
...prev,
|
||||||
...prev,
|
selectedOrganization: e.target.value,
|
||||||
selectedOrganization: e.target.value,
|
}))
|
||||||
}))
|
}
|
||||||
}
|
disabled={orgLoading}
|
||||||
disabled={orgLoading}
|
>
|
||||||
>
|
<option value="">All Organizations</option>
|
||||||
<option value="">All Organizations</option>
|
{organizations?.map((org, ind) => (
|
||||||
{organizations?.map((org, ind) => (
|
<option key={`${org.id}-${ind}`} value={org.id}>
|
||||||
<option key={`${org.id}-${ind}`} value={org.id}>
|
{org.name}
|
||||||
{org.name}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-6">
|
<div className="col-12 col-sm-6">
|
||||||
{/* Search Input */}
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
className="form-control form-control-sm"
|
||||||
className="form-control form-control-sm"
|
placeholder="Search Employee..."
|
||||||
placeholder="Search Employee..."
|
value={searchTerm}
|
||||||
value={searchTerm}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -224,7 +220,7 @@ const AttendancePage = () => {
|
|||||||
{selectedProject ? (
|
{selectedProject ? (
|
||||||
<>
|
<>
|
||||||
{activeTab === "all" && (
|
{activeTab === "all" && (
|
||||||
<div className="tab-pane fade show active py-0 mx-5">
|
<div className="tab-pane fade show active py-0 mx-2">
|
||||||
<Attendance
|
<Attendance
|
||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
getRole={getRole}
|
getRole={getRole}
|
||||||
@ -234,7 +230,7 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeTab === "logs" && (
|
{activeTab === "logs" && (
|
||||||
<div className="tab-pane fade show active py-0">
|
<div className="tab-pane fade p-3 show active py-0">
|
||||||
<AttendanceLog
|
<AttendanceLog
|
||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
@ -243,7 +239,7 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeTab === "regularization" && DoRegularized && (
|
{activeTab === "regularization" && DoRegularized && (
|
||||||
<div className="tab-pane fade show active py-0">
|
<div className="tab-pane fade p-3 show active py-0">
|
||||||
<Regularization
|
<Regularization
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
organizationId={appliedFilters.selectedOrganization}
|
organizationId={appliedFilters.selectedOrganization}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const TaskPlanning = () => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="card py-2 ">
|
<div className="card py-2 page-min-h">
|
||||||
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
|
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<p className="badge bg-label-secondary m-0">Service not assigned</p>
|
<p className="badge bg-label-secondary m-0">Service not assigned</p>
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React from "react";
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
contactsFilter,
|
contactsFilter,
|
||||||
@ -8,70 +13,109 @@ import {
|
|||||||
import { useContactFilter } from "../../hooks/useDirectory";
|
import { useContactFilter } from "../../hooks/useDirectory";
|
||||||
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
|
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
|
||||||
import SelectMultiple from "../../components/common/SelectMultiple";
|
import SelectMultiple from "../../components/common/SelectMultiple";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
const ContactFilterPanel = ({ onApply, clearFilter }) => {
|
const ContactFilterPanel = forwardRef(
|
||||||
const { data, isError, isLoading, error, isFetched, isFetching } =
|
({ onApply, clearFilter, setFilterdata }, ref) => {
|
||||||
useContactFilter();
|
const { data, isError, isLoading, error, isFetched, isFetching } =
|
||||||
|
useContactFilter();
|
||||||
|
const { status } = useParams();
|
||||||
|
|
||||||
const methods = useForm({
|
useEffect(() => {
|
||||||
resolver: zodResolver(contactsFilter),
|
return () => {
|
||||||
defaultValues: defaultContactFilter,
|
closePanel();
|
||||||
});
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const closePanel = () => {
|
const dynamicdefaultContactFilter = useMemo(() => {
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
return {
|
||||||
};
|
...defaultContactFilter,
|
||||||
|
bucketIds: defaultContactFilter.bucketIds || [],
|
||||||
|
categoryIds: defaultContactFilter.categoryIds || [],
|
||||||
|
};
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const { register, handleSubmit, reset, watch } = methods;
|
const methods = useForm({
|
||||||
|
resolver: zodResolver(contactsFilter),
|
||||||
|
defaultValues: dynamicdefaultContactFilter,
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const { handleSubmit, reset, setValue, getValues } = methods;
|
||||||
onApply(formData);
|
|
||||||
closePanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
useImperativeHandle(ref, () => ({
|
||||||
reset(defaultContactFilter);
|
resetFieldValue: (name, value) => {
|
||||||
onApply(defaultContactFilter);
|
setTimeout(() => {
|
||||||
closePanel();
|
if (value !== undefined) {
|
||||||
};
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...getValues(), [name]: defaultContactFilter[name] });
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
getValues,
|
||||||
|
}));
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
useEffect(() => {
|
||||||
if (isError && isFetched)
|
if (data && setFilterdata) {
|
||||||
return <div>Something went wrong Here- {error.message} </div>;
|
setFilterdata(data);
|
||||||
return (
|
}
|
||||||
<FormProvider {...methods}>
|
}, [data, setFilterdata]);
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
|
||||||
<div className="row g-2">
|
|
||||||
<SelectMultiple
|
|
||||||
name="bucketIds"
|
|
||||||
label="Buckets :"
|
|
||||||
options={data.buckets}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
<SelectMultiple
|
|
||||||
name="categoryIds"
|
|
||||||
label="Contact Category :"
|
|
||||||
options={data.contactCategories}
|
|
||||||
labelKey={(item) => item.name}
|
|
||||||
valueKey="id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-label-secondary btn-sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="btn btn-primary btn-sm">
|
|
||||||
Apply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContactFilterPanel;
|
const closePanel = () => {
|
||||||
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
onApply(formData);
|
||||||
|
// closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
reset(defaultContactFilter);
|
||||||
|
onApply(defaultContactFilter);
|
||||||
|
// closePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||||
|
if (isError && isFetched)
|
||||||
|
return <div>Something went wrong — {error?.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
|
<div className="row g-2">
|
||||||
|
<SelectMultiple
|
||||||
|
name="bucketIds"
|
||||||
|
label="Buckets:"
|
||||||
|
options={data?.buckets || []}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
<SelectMultiple
|
||||||
|
name="categoryIds"
|
||||||
|
label="Contact Category:"
|
||||||
|
options={data?.contactCategories || []}
|
||||||
|
labelKey={(item) => item.name}
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-label-secondary btn-sm"
|
||||||
|
onClick={handleClose}
|
||||||
|
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm" >
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ContactFilterPanel;
|
||||||
@ -1,26 +1,30 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useContactList } from "../../hooks/useDirectory";
|
import { useContactList } from "../../hooks/useDirectory";
|
||||||
import { useDirectoryContext } from "./DirectoryPage";
|
import { useDirectoryContext } from "./DirectoryPage";
|
||||||
import CardViewContact from "../../components/Directory/CardViewContact";
|
import CardViewContact from "../../components/Directory/CardViewContact";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
import ContactFilterPanel from "./ContactFilterPanel";
|
import ContactFilterPanel from "./ContactFilterPanel";
|
||||||
|
import ContactFilterChips from "../../components/Directory/ContactFilterChips";
|
||||||
import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
|
import { defaultContactFilter } from "../../components/Directory/DirectorySchema";
|
||||||
import { useDebounce } from "../../utils/appUtils";
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
import ListViewContact from "../../components/Directory/ListViewContact";
|
import ListViewContact from "../../components/Directory/ListViewContact";
|
||||||
import { CardViewContactSkeleton, ListViewContactSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
import Loader from "../../components/common/Loader";
|
||||||
|
|
||||||
// Utility function to format contacts for CSV export
|
// Utility for CSV export
|
||||||
const formatExportData = (contacts) => {
|
const formatExportData = (contacts) => {
|
||||||
return contacts.map(contact => ({
|
return contacts.map((contact) => ({
|
||||||
Email: contact.contactEmails?.map(e => e.emailAddress).join(", ") || "",
|
Email: contact.contactEmails?.map((e) => e.emailAddress).join(", ") || "",
|
||||||
Phone: contact.contactPhones?.map(p => p.phoneNumber).join(", ") || "",
|
Phone: contact.contactPhones?.map((p) => p.phoneNumber).join(", ") || "",
|
||||||
Created: contact.createdAt ? new Date(contact.createdAt).toLocaleString() : "",
|
Created: contact.createdAt
|
||||||
|
? new Date(contact.createdAt).toLocaleString()
|
||||||
|
: "",
|
||||||
Location: contact.address || "",
|
Location: contact.address || "",
|
||||||
Organization: contact.organization || "",
|
Organization: contact.organization || "",
|
||||||
Category: contact.contactCategory?.name || "",
|
Category: contact.contactCategory?.name || "",
|
||||||
Tags: contact.tags?.map(t => t.name).join(", ") || "",
|
Tags: contact.tags?.map((t) => t.name).join(", ") || "",
|
||||||
Buckets: contact.bucketIds?.join(", ") || "",
|
Buckets: contact.bucketIds?.join(", ") || "",
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@ -28,8 +32,10 @@ const formatExportData = (contacts) => {
|
|||||||
const ContactsPage = ({ projectId, searchText, onExport }) => {
|
const ContactsPage = ({ projectId, searchText, onExport }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [filters, setFilter] = useState(defaultContactFilter);
|
const [filters, setFilter] = useState(defaultContactFilter);
|
||||||
|
const [filterData, setFilterdata] = useState(null);
|
||||||
const debouncedSearch = useDebounce(searchText, 500);
|
const debouncedSearch = useDebounce(searchText, 500);
|
||||||
const { showActive, gridView } = useDirectoryContext();
|
const { showActive, gridView } = useDirectoryContext();
|
||||||
|
const updatedRef = useRef();
|
||||||
const { data, isError, isLoading, error } = useContactList(
|
const { data, isError, isLoading, error } = useContactList(
|
||||||
showActive,
|
showActive,
|
||||||
projectId,
|
projectId,
|
||||||
@ -40,13 +46,19 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
);
|
);
|
||||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||||
|
|
||||||
|
// clear filters
|
||||||
const clearFilter = () => setFilter(defaultContactFilter);
|
const clearFilter = () => setFilter(defaultContactFilter);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowTrigger(true);
|
setShowTrigger(true);
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Contacts Filters",
|
"Contacts Filters",
|
||||||
<ContactFilterPanel onApply={setFilter} clearFilter={clearFilter} />
|
<ContactFilterPanel
|
||||||
|
ref={updatedRef}
|
||||||
|
onApply={setFilter}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
setFilterdata={setFilterdata}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -55,7 +67,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 🔹 Format contacts for export
|
// export data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.data && onExport) {
|
if (data?.data && onExport) {
|
||||||
onExport(formatExportData(data.data));
|
onExport(formatExportData(data.data));
|
||||||
@ -68,15 +80,54 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoveChip = (key, id) => {
|
||||||
|
setFilter((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
|
||||||
|
if (Array.isArray(updated[key])) {
|
||||||
|
updated[key] = updated[key].filter((v) => v !== id);
|
||||||
|
updatedRef.current?.resetFieldValue(key, updated[key]);
|
||||||
|
} else {
|
||||||
|
updated[key] = null;
|
||||||
|
updatedRef.current?.resetFieldValue(key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
// if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row mt-5">
|
<div className="row mt-4">
|
||||||
|
{/* Chips Section */}
|
||||||
|
<div className="col-12 mb-2">
|
||||||
|
<ContactFilterChips
|
||||||
|
filters={filters}
|
||||||
|
filterData={filterData}
|
||||||
|
removeFilterChip={handleRemoveChip}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid / List View */}
|
||||||
{gridView ? (
|
{gridView ? (
|
||||||
<>
|
<>
|
||||||
|
{isLoading && <Loader />}
|
||||||
|
|
||||||
|
{data?.data?.length === 0 && (
|
||||||
|
<div className="py-4 text-center">
|
||||||
|
{searchText
|
||||||
|
? `No contact found for "${searchText}"`
|
||||||
|
: "No contacts found"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{data?.data?.map((contact) => (
|
{data?.data?.map((contact) => (
|
||||||
<div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
|
<div
|
||||||
|
key={contact.id}
|
||||||
|
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
|
||||||
|
>
|
||||||
<CardViewContact IsActive={showActive} contact={contact} />
|
<CardViewContact IsActive={showActive} contact={contact} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -95,6 +146,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<ListViewContact
|
<ListViewContact
|
||||||
data={data?.data}
|
data={data?.data}
|
||||||
|
isLoading={isLoading}
|
||||||
Pagination={
|
Pagination={
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@ -109,4 +161,4 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactsPage;
|
export default ContactsPage;
|
||||||
@ -139,9 +139,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
<ul className="nav nav-tabs">
|
<ul className="nav nav-tabs">
|
||||||
<li className="nav-item cursor-pointer">
|
<li className="nav-item cursor-pointer">
|
||||||
<a
|
<a
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "notes" ? "active" : ""
|
||||||
activeTab === "notes" ? "active" : ""
|
} fs-6`}
|
||||||
} fs-6`}
|
|
||||||
onClick={(e) => handleTabClick("notes", e)}
|
onClick={(e) => handleTabClick("notes", e)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-notepad bx-sm me-1_5"></i>
|
<i className="bx bx-notepad bx-sm me-1_5"></i>
|
||||||
@ -150,9 +149,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
</li>
|
</li>
|
||||||
<li className="nav-item cursor-pointer">
|
<li className="nav-item cursor-pointer">
|
||||||
<a
|
<a
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "contacts" ? "active" : ""
|
||||||
activeTab === "contacts" ? "active" : ""
|
} fs-6`}
|
||||||
} fs-6`}
|
|
||||||
onClick={(e) => handleTabClick("contacts", e)}
|
onClick={(e) => handleTabClick("contacts", e)}
|
||||||
>
|
>
|
||||||
<i className="bx bxs-contact bx-sm me-1_5"></i>
|
<i className="bx bxs-contact bx-sm me-1_5"></i>
|
||||||
@ -168,105 +166,84 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
{activeTab === "notes" && (
|
{activeTab === "notes" && (
|
||||||
<div className="col-8 col-md-3">
|
<div className="col-8 col-md-3">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search notes..."
|
placeholder="Search notes..."
|
||||||
value={searchNote}
|
value={searchNote}
|
||||||
onChange={(e) => setSearchNote(e.target.value)}
|
onChange={(e) => setSearchNote(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "contacts" && (
|
{activeTab === "contacts" && (
|
||||||
<div className="d-flex align-items-center gap-3">
|
<div className="d-flex align-items-center gap-3">
|
||||||
<div className="col-12 col-md-8 d-flex flex-row gap-2">
|
<div className="col-12 col-md-8 d-flex flex-row gap-2">
|
||||||
<div className="col-7 col-md-4">
|
<div className="col-7 col-md-4">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search contacts..."
|
placeholder="Search contacts..."
|
||||||
value={searchContact}
|
value={searchContact}
|
||||||
onChange={(e) => setsearchContact(e.target.value)}
|
onChange={(e) => setsearchContact(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm p-1 ${
|
className={`btn btn-sm p-1 ${gridView ? " btn-primary" : " btn-outline-primary"
|
||||||
!gridView ? "btn-primary" : "btn-outline-primary"
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setGridView(false)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-list-ul"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`btn btn-sm p-1 ${
|
|
||||||
gridView ? " btn-primary" : " btn-outline-primary"
|
|
||||||
}`}
|
|
||||||
onClick={() => setGridView(true)}
|
onClick={() => setGridView(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-grid-alt"></i>
|
<i className="bx bx-grid-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<div className="form-check form-switch d-flex align-items-center d-none d-md-flex">
|
<button
|
||||||
<input
|
className={`btn btn-sm p-1 ${!gridView ? "btn-primary" : "btn-outline-primary"
|
||||||
type="checkbox"
|
}`}
|
||||||
className="form-check-input"
|
onClick={() => setGridView(false)}
|
||||||
role="switch"
|
|
||||||
id="inactiveEmployeesCheckbox"
|
|
||||||
checked={showActive}
|
|
||||||
onChange={(e) => setShowActive(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label ms-2"
|
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
|
||||||
>
|
>
|
||||||
{showActive ? "Active" : "Inactive"} Contacts
|
<i className="bx bx-list-ul"></i>
|
||||||
</label>
|
</button>
|
||||||
|
|
||||||
|
<div className="form-check form-switch d-flex align-items-end d-none d-md-flex">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
role="switch"
|
||||||
|
id="inactiveEmployeesCheckbox"
|
||||||
|
checked={showActive}
|
||||||
|
onChange={(e) => setShowActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label ms-2"
|
||||||
|
htmlFor="inactiveEmployeesCheckbox"
|
||||||
|
>
|
||||||
|
{showActive ? "Active" : "In-active"} Contacts
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2">
|
<div className="col-12 col-md-2 d-flex justify-content-end align-items-center gap-2">
|
||||||
<div className={`form-check form-switch d-flex align-items-center ${activeTab === "contacts" ? " d-flex d-md-none m-0":"d-none" }`}>
|
<div className=" btn-group">
|
||||||
<input
|
<button
|
||||||
type="checkbox"
|
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
||||||
className="form-check-input"
|
type="button"
|
||||||
role="switch"
|
data-bs-toggle="dropdown"
|
||||||
id="inactiveEmployeesCheckbox"
|
aria-expanded="false"
|
||||||
checked={showActive}
|
>
|
||||||
onChange={(e) => setShowActive(e.target.checked)}
|
<i className="bx bx-export me-2 bx-sm"></i>Export
|
||||||
/>
|
</button>
|
||||||
<label
|
<ul className="dropdown-menu">
|
||||||
className="form-check-label ms-2"
|
<li>
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
<a
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
onClick={() => handleExport("csv")}
|
||||||
>
|
>
|
||||||
{showActive ? "Active" : "Inactive"} Contacts
|
<i className="bx bx-file me-1"></i> CSV
|
||||||
</label>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
<div className=" btn-group">
|
</ul>
|
||||||
<button
|
</div>
|
||||||
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className="bx bx-export me-2 bx-sm"></i>Export
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item cursor-pointer"
|
|
||||||
onClick={() => handleExport("csv")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-file me-1"></i> CSV
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,77 +1,117 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React from "react";
|
import React, { useEffect, useImperativeHandle, forwardRef, useMemo } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
defaultNotesFilter,
|
defaultNotesFilter,
|
||||||
notesFilter,
|
notesFilter,
|
||||||
} from "../../components/Directory/DirectorySchema";
|
} from "../../components/Directory/DirectorySchema";
|
||||||
import { useContactFilter, useNoteFilter } from "../../hooks/useDirectory";
|
import { useNoteFilter } from "../../hooks/useDirectory";
|
||||||
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
|
import { ExpenseFilterSkeleton } from "../../components/Expenses/ExpenseSkeleton";
|
||||||
import SelectMultiple from "../../components/common/SelectMultiple";
|
import SelectMultiple from "../../components/common/SelectMultiple";
|
||||||
|
|
||||||
const NoteFilterPanel = ({ onApply, clearFilter }) => {
|
const NoteFilterPanel = forwardRef(({ onApply, clearFilter, setFilterdata }, ref) => {
|
||||||
const { data, isError, isLoading, error, isFetched, isFetching } =
|
const { data, isError, isLoading, error, isFetched, isFetching } = useNoteFilter();
|
||||||
useNoteFilter();
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
closePanel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
//Add this for Filter chip remover
|
||||||
|
const dynamicdefaultNotesFilter = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...defaultNotesFilter,
|
||||||
|
bucketIds: defaultNotesFilter.bucketIds || [],
|
||||||
|
categoryIds: defaultNotesFilter.categoryIds || [],
|
||||||
|
};
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(notesFilter),
|
resolver: zodResolver(notesFilter),
|
||||||
defaultValues: defaultNotesFilter,
|
defaultValues: dynamicdefaultNotesFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, reset, setValue, getValues } = methods;
|
||||||
|
|
||||||
const closePanel = () => {
|
const closePanel = () => {
|
||||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { register, handleSubmit, reset, watch } = methods;
|
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
onApply(formData);
|
onApply(formData);
|
||||||
closePanel();
|
// closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
reset(defaultNotesFilter);
|
reset(defaultNotesFilter);
|
||||||
onApply(defaultNotesFilter);
|
onApply(defaultNotesFilter);
|
||||||
closePanel();
|
// closePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
//Add this for Filter chip remover
|
||||||
if (isError && isFetched)
|
useImperativeHandle(ref, () => ({
|
||||||
return <div>Something went wrong Here- {error.message} </div>;
|
resetFieldValue: (name, value) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setValue(name, value);
|
||||||
|
} else {
|
||||||
|
reset({ ...getValues(), [name]: defaultNotesFilter[name] });
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
getValues,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && setFilterdata) {
|
||||||
|
setFilterdata(data);
|
||||||
|
}
|
||||||
|
}, [data, setFilterdata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
<div className="row g-2">
|
{isLoading || isFetching ? (
|
||||||
<SelectMultiple
|
<ExpenseFilterSkeleton />
|
||||||
name="createdByIds"
|
) : isError && isFetched ? (
|
||||||
label="Created By :"
|
<div>Something went wrong here: {error?.message}</div>
|
||||||
options={data.createdBy}
|
) : (
|
||||||
labelKey="name"
|
<>
|
||||||
valueKey="id"
|
<div className="row g-2">
|
||||||
/>
|
<SelectMultiple
|
||||||
<SelectMultiple
|
name="createdByIds"
|
||||||
name="organizations"
|
label="Created By :"
|
||||||
label="Organization:"
|
options={data?.createdBy || []}
|
||||||
options={data.organizations}
|
labelKey="name"
|
||||||
labelKey={(item) => item.name}
|
valueKey="id"
|
||||||
valueKey="id"
|
/>
|
||||||
/>
|
<SelectMultiple
|
||||||
</div>
|
name="organizations"
|
||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
label="Organization:"
|
||||||
<button
|
options={data?.organizations || []}
|
||||||
type="button"
|
labelKey={(item) => item.name}
|
||||||
className="btn btn-label-secondary btn-sm"
|
valueKey="id"
|
||||||
onClick={handleClose}
|
/>
|
||||||
>
|
</div>
|
||||||
Clear
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
</button>
|
<button
|
||||||
<button type="submit" className="btn btn-primary btn-sm">
|
type="button"
|
||||||
Apply
|
className="btn btn-label-secondary btn-sm"
|
||||||
</button>
|
onClick={handleClose}
|
||||||
</div>
|
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm" >
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default NoteFilterPanel;
|
export default NoteFilterPanel;
|
||||||
@ -1,5 +1,4 @@
|
|||||||
// NotesPage.jsx
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useNotes } from "../../hooks/useDirectory";
|
import { useNotes } from "../../hooks/useDirectory";
|
||||||
import NoteFilterPanel from "./NoteFilterPanel";
|
import NoteFilterPanel from "./NoteFilterPanel";
|
||||||
@ -9,11 +8,14 @@ import { useDebounce } from "../../utils/appUtils";
|
|||||||
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
|
import NoteCardDirectoryEditable from "../../components/Directory/NoteCardDirectoryEditable";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
import { NoteCardSkeleton } from "../../components/Directory/DirectoryPageSkeleton";
|
||||||
|
import NoteFilterChips from "../../components/Directory/NoteFilterChips";
|
||||||
|
|
||||||
const NotesPage = ({ projectId, searchText, onExport }) => {
|
const NotesPage = ({ projectId, searchText, onExport }) => {
|
||||||
const [filters, setFilter] = useState(defaultNotesFilter);
|
const [filters, setFilter] = useState(defaultNotesFilter);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const debouncedSearch = useDebounce(searchText, 500);
|
const debouncedSearch = useDebounce(searchText, 500);
|
||||||
|
const [filterData, setFilterdata] = useState(null);
|
||||||
|
const updatedRef = useRef();
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useNotes(
|
const { data, isLoading, isError, error } = useNotes(
|
||||||
projectId,
|
projectId,
|
||||||
@ -33,7 +35,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
setShowTrigger(true);
|
setShowTrigger(true);
|
||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Notes Filters",
|
"Notes Filters",
|
||||||
<NoteFilterPanel onApply={setFilter} clearFilter={clearFilter} />
|
<NoteFilterPanel
|
||||||
|
ref={updatedRef} //Call here
|
||||||
|
onApply={setFilter}
|
||||||
|
clearFilter={clearFilter}
|
||||||
|
setFilterdata={setFilterdata}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -42,11 +49,27 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 🔹 Format data for export
|
const handleRemoveChip = (key, id) => {
|
||||||
|
setFilter((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
|
||||||
|
if (Array.isArray(updated[key])) {
|
||||||
|
updated[key] = updated[key].filter((v) => v !== id);
|
||||||
|
updatedRef.current?.resetFieldValue(key, updated[key]); //IMP
|
||||||
|
} else {
|
||||||
|
updated[key] = null;
|
||||||
|
updatedRef.current?.resetFieldValue(key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format data for export
|
||||||
const formatExportData = (notes) => {
|
const formatExportData = (notes) => {
|
||||||
return notes.map((n) => ({
|
return notes.map((n) => ({
|
||||||
ContactName: n.contactName || "",
|
ContactName: n.contactName || "",
|
||||||
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "", // strip HTML tags
|
Note: n.note ? n.note.replace(/<[^>]+>/g, "") : "",
|
||||||
Organization: n.organizationName || "",
|
Organization: n.organizationName || "",
|
||||||
CreatedBy: n.createdBy
|
CreatedBy: n.createdBy
|
||||||
? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim()
|
? `${n.createdBy.firstName || ""} ${n.createdBy.lastName || ""}`.trim()
|
||||||
@ -59,7 +82,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔹 Pass formatted notes to parent for export
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.data && onExport) {
|
if (data?.data && onExport) {
|
||||||
onExport(formatExportData(data.data));
|
onExport(formatExportData(data.data));
|
||||||
@ -77,6 +99,12 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-column text-start mt-3">
|
<div className="d-flex flex-column text-start mt-3">
|
||||||
|
<NoteFilterChips
|
||||||
|
filters={filters}
|
||||||
|
filterData={filterData}
|
||||||
|
removeFilterChip={handleRemoveChip}
|
||||||
|
/>
|
||||||
|
|
||||||
{data?.data?.length > 0 ? (
|
{data?.data?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{data.data.map((noteItem) => (
|
{data.data.map((noteItem) => (
|
||||||
@ -96,7 +124,6 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// Card for "No notes available"
|
|
||||||
<div
|
<div
|
||||||
className="card text-center d-flex align-items-center justify-content-center"
|
className="card text-center d-flex align-items-center justify-content-center"
|
||||||
style={{ height: "200px" }}
|
style={{ height: "200px" }}
|
||||||
@ -104,9 +131,9 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
<p className="text-muted mb-0">
|
<p className="text-muted mb-0">
|
||||||
{debouncedSearch
|
{debouncedSearch
|
||||||
? `No notes found matching "${searchText}"`
|
? `No notes found matching "${searchText}"`
|
||||||
: Object.keys(filters).some((k) => filters[k] && filters[k].length)
|
: Object.keys(filters).some((k) => filters[k]?.length)
|
||||||
? "No notes found for the applied filters."
|
? "No notes found for the applied filters."
|
||||||
: "No notes available."}
|
: "No notes available."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -114,4 +141,4 @@ const NotesPage = ({ projectId, searchText, onExport }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotesPage;
|
export default NotesPage;
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
import React, { createContext, useContext, useState, useEffect, useRef } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import ExpenseList from "../../components/Expenses/ExpenseList";
|
|
||||||
import ViewExpense from "../../components/Expenses/ViewExpense";
|
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
import ExpenseList from "../../components/Expenses/ExpenseList";
|
||||||
|
import ViewExpense from "../../components/Expenses/ViewExpense";
|
||||||
import ManageExpense from "../../components/Expenses/ManageExpense";
|
import ManageExpense from "../../components/Expenses/ManageExpense";
|
||||||
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
|
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
|
||||||
|
import ExpenseFilterChips from "../../components/Expenses/ExpenseFilterChips";
|
||||||
|
|
||||||
// Context & Hooks
|
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import {
|
import {
|
||||||
@ -20,11 +19,8 @@ import {
|
|||||||
VIEW_SELF_EXPENSE,
|
VIEW_SELF_EXPENSE,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
|
|
||||||
// Schema & Defaults
|
import { defaultFilter, SearchSchema } from "../../components/Expenses/ExpenseSchema";
|
||||||
import {
|
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||||
defaultFilter,
|
|
||||||
SearchSchema,
|
|
||||||
} from "../../components/Expenses/ExpenseSchema";
|
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
export const ExpenseContext = createContext();
|
export const ExpenseContext = createContext();
|
||||||
@ -41,10 +37,10 @@ const ExpensePage = () => {
|
|||||||
(store) => store.localVariables.projectId
|
(store) => store.localVariables.projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filters, setFilter] = useState();
|
const [filters, setFilters] = useState(defaultFilter);
|
||||||
const [groupBy, setGroupBy] = useState("transactionDate");
|
const [groupBy, setGroupBy] = useState("transactionDate");
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const filterPanelRef = useRef();
|
||||||
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
||||||
IsOpen: null,
|
IsOpen: null,
|
||||||
expenseId: null,
|
expenseId: null,
|
||||||
@ -63,19 +59,22 @@ const ExpensePage = () => {
|
|||||||
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
|
||||||
const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE);
|
const IsViewAll = useHasUserPermission(VIEW_ALL_EXPNESE);
|
||||||
const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE);
|
const IsViewSelf = useHasUserPermission(VIEW_SELF_EXPENSE);
|
||||||
|
|
||||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||||
|
const [filterData, setFilterdata] = useState(defaultFilter);
|
||||||
const methods = useForm({
|
const removeFilterChip = (key, id) => {
|
||||||
resolver: zodResolver(SearchSchema),
|
setFilters((prev) => {
|
||||||
defaultValues: defaultFilter,
|
const updated = { ...prev };
|
||||||
});
|
if (Array.isArray(updated[key])) {
|
||||||
|
updated[key] = updated[key].filter((v) => v !== id);
|
||||||
const { reset } = methods;
|
filterPanelRef.current?.resetFieldValue(key, updated[key]);
|
||||||
|
} else if (key === "dateRange") {
|
||||||
const clearFilter = () => {
|
updated.startDate = null;
|
||||||
setFilter(defaultFilter);
|
updated.endDate = null;
|
||||||
reset();
|
filterPanelRef.current?.resetFieldValue("startDate", null);
|
||||||
|
filterPanelRef.current?.resetFieldValue("endDate", null);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -84,9 +83,10 @@ const ExpensePage = () => {
|
|||||||
setOffcanvasContent(
|
setOffcanvasContent(
|
||||||
"Expense Filters",
|
"Expense Filters",
|
||||||
<ExpenseFilterPanel
|
<ExpenseFilterPanel
|
||||||
onApply={setFilter}
|
ref={filterPanelRef}
|
||||||
|
onApply={setFilters}
|
||||||
handleGroupBy={setGroupBy}
|
handleGroupBy={setGroupBy}
|
||||||
clearFilter={clearFilter}
|
setFilterdata={setFilterdata}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -101,6 +101,8 @@ const ExpensePage = () => {
|
|||||||
setViewExpense,
|
setViewExpense,
|
||||||
setManageExpenseModal,
|
setManageExpenseModal,
|
||||||
setDocumentView,
|
setDocumentView,
|
||||||
|
filterData,
|
||||||
|
removeFilterChip
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -115,20 +117,18 @@ const ExpensePage = () => {
|
|||||||
<div className="card my-3 px-sm-4 px-0">
|
<div className="card my-3 px-sm-4 px-0">
|
||||||
<div className="card-body py-2 px-3">
|
<div className="card-body py-2 px-3">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col-6 ">
|
<div className="col-6">
|
||||||
<div className="d-flex align-items-center">
|
<input
|
||||||
<input
|
type="search"
|
||||||
type="search"
|
className="form-control form-control-sm w-auto"
|
||||||
className="form-control form-control-sm w-auto"
|
placeholder="Search Expense"
|
||||||
placeholder="Search Expense"
|
value={searchText}
|
||||||
aria-describedby="search-label"
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
value={searchText}
|
/>
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-6 text-end mt-2 mt-sm-0">
|
<div className="col-6 text-end mt-2 mt-sm-0">
|
||||||
|
|
||||||
{IsCreatedAble && (
|
{IsCreatedAble && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
@ -151,6 +151,8 @@ const ExpensePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ExpenseList
|
<ExpenseList
|
||||||
filters={filters}
|
filters={filters}
|
||||||
groupBy={groupBy}
|
groupBy={groupBy}
|
||||||
@ -161,7 +163,7 @@ const ExpensePage = () => {
|
|||||||
<div className="card text-center py-1">
|
<div className="card text-center py-1">
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5" />
|
<i className="fa-solid fa-triangle-exclamation fs-5" />
|
||||||
<p>
|
<p>
|
||||||
Access Denied: You don't have permission to perform this action !
|
Access Denied: You don't have permission to perform this action!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -654,3 +654,15 @@ nav.layout-navbar.navbar-active::after {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-style .landing-hero {
|
||||||
|
background: linear-gradient(138.18deg, #eae8fd, #ede7e7 94.44%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green {
|
||||||
|
color: #49bf3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue {
|
||||||
|
color: var(--bs-blue);
|
||||||
|
}
|
||||||
|
|||||||
@ -54,15 +54,27 @@ const LandingPage = () => {
|
|||||||
>
|
>
|
||||||
<i className="tf-icons bx bx-menu bx-lg align-middle text-heading fw-medium"></i>
|
<i className="tf-icons bx bx-menu bx-lg align-middle text-heading fw-medium"></i>
|
||||||
</button>
|
</button>
|
||||||
{/* Mobile menu toggle: End*/}
|
|
||||||
<a href="/" className="app-brand-link">
|
{/* <a href="/" className="app-brand-link">
|
||||||
<span className="app-brand-logo demo">
|
<span className="app-brand-logo demo">
|
||||||
<img src="/img/brand/marco.png" width={50}></img>
|
<img src="/img/brand/marco.png" width={50}></img>
|
||||||
</span>
|
</span>
|
||||||
<span className="app-brand-text demo menu-text fw-bold ms-2 ps-1 ">
|
<span className="app-brand-text demo menu-text fw-bold ms-2 ps-1 ">
|
||||||
{/* <Link> */} PMS
|
PMS
|
||||||
{/* </Link> */}
|
|
||||||
</span>
|
</span>
|
||||||
|
</a> */}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="app-brand-link fw-bold navbar-brand text-green fs-5"
|
||||||
|
>
|
||||||
|
<span class="app-brand-logo demo">
|
||||||
|
<img src="/img/brand/marco.png" width="50" />
|
||||||
|
</span>
|
||||||
|
<span class="text-blue">OnField</span>
|
||||||
|
<span>Work</span>
|
||||||
|
<span class="text-dark">.com</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/* Menu logo wrapper: End */}
|
{/* Menu logo wrapper: End */}
|
||||||
@ -226,7 +238,7 @@ const LandingPage = () => {
|
|||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
<SwiperSlide>
|
<SwiperSlide>
|
||||||
<SwaperSlideContent
|
<SwaperSlideContent
|
||||||
ImageUrl="/app/dashboard-light-04.png"
|
ImageUrl="/img/app/dashboard-light-09.png"
|
||||||
Title="Role-based Permissions"
|
Title="Role-based Permissions"
|
||||||
Body="Securely control access with customizable roles and permissions."
|
Body="Securely control access with customizable roles and permissions."
|
||||||
></SwaperSlideContent>
|
></SwaperSlideContent>
|
||||||
@ -367,7 +379,7 @@ const LandingPage = () => {
|
|||||||
</div>{" "}
|
</div>{" "}
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img src="/img/icons/inventory.svg" alt="keyboard" />
|
<img src="/img/icons/user.svg" alt="keyboard" />
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Inventory Management</h5>
|
<h5 className="mb-2">Inventory Management</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -436,7 +448,7 @@ const LandingPage = () => {
|
|||||||
{" "}
|
{" "}
|
||||||
<SwaperBlogContent
|
<SwaperBlogContent
|
||||||
ImageUrl="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQs28_JnxJUKdAgaZsiWW4NyekVmfmLtpUtaA&s"
|
ImageUrl="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQs28_JnxJUKdAgaZsiWW4NyekVmfmLtpUtaA&s"
|
||||||
Title="Transforming Residential Project Delivery with Marco PMS"
|
Title="Transforming Residential Project Delivery with OnFieldWork.com"
|
||||||
Body="Sunrise Builders struggled with delays, cost overruns, and lack of transparency in their multi-phase township project. Different contractors for civil, electrical, and finishing works operated in silos, and communication gaps with the Project Management Consultant (PMC) often led to rework and disputes."
|
Body="Sunrise Builders struggled with delays, cost overruns, and lack of transparency in their multi-phase township project. Different contractors for civil, electrical, and finishing works operated in silos, and communication gaps with the Project Management Consultant (PMC) often led to rework and disputes."
|
||||||
></SwaperBlogContent>
|
></SwaperBlogContent>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
@ -627,7 +639,7 @@ const LandingPage = () => {
|
|||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
aria-controls="accordionOne"
|
aria-controls="accordionOne"
|
||||||
>
|
>
|
||||||
What is MarcoPMS?
|
What is OnFieldWork.com?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -684,7 +696,7 @@ const LandingPage = () => {
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="accordionThree"
|
aria-controls="accordionThree"
|
||||||
>
|
>
|
||||||
How secure is Marco PMS?
|
How secure is OnFieldWork.com?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
@ -694,7 +706,7 @@ const LandingPage = () => {
|
|||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body text-start">
|
<div className="accordion-body text-start">
|
||||||
Security is at the core of Marco PMS. We use
|
Security is at the core of OnFieldWork.com. We use
|
||||||
industry-standard encryption (SSL/TLS) to protect data
|
industry-standard encryption (SSL/TLS) to protect data
|
||||||
in transit and advanced encryption to safeguard data at
|
in transit and advanced encryption to safeguard data at
|
||||||
rest. Role-based access controls ensure that only
|
rest. Role-based access controls ensure that only
|
||||||
@ -754,13 +766,13 @@ const LandingPage = () => {
|
|||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body text-start">
|
<div className="accordion-body text-start">
|
||||||
Marco PMS operate under a proprietary license combined
|
OnFieldWork.com operate under a proprietary license
|
||||||
with a subscription model. This means customers don’t
|
combined with a subscription model. This means customers
|
||||||
own the software but are granted the right to access and
|
don’t own the software but are granted the right to
|
||||||
use it through the cloud under our Terms of Service.
|
access and use it through the cloud under our Terms of
|
||||||
Depending on the plan, licensing may be based on users,
|
Service. Depending on the plan, licensing may be based
|
||||||
features, or usage, and you can upgrade, downgrade, or
|
on users, features, or usage, and you can upgrade,
|
||||||
cancel at any time. non!
|
downgrade, or cancel at any time. non!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -774,7 +786,7 @@ const LandingPage = () => {
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="accordionSix"
|
aria-controls="accordionSix"
|
||||||
>
|
>
|
||||||
Can I customize Marco PMS for my business needs?
|
Can I customize OnFieldWork.com for my business needs?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
@ -784,11 +796,11 @@ const LandingPage = () => {
|
|||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body text-start">
|
<div className="accordion-body text-start">
|
||||||
Yes, Marco PMS is designed to be flexible and adaptable.
|
Yes, OnFieldWork.com is designed to be flexible and
|
||||||
You can customize workflows, user roles, permissions,
|
adaptable. You can customize workflows, user roles,
|
||||||
and reporting to match your organization’s unique
|
permissions, and reporting to match your organization’s
|
||||||
processes. Depending on your plan, we also support
|
unique processes. Depending on your plan, we also
|
||||||
advanced customization such as integrating with
|
support advanced customization such as integrating with
|
||||||
third-party tools, adding custom fields, and tailoring
|
third-party tools, adding custom fields, and tailoring
|
||||||
modules to fit your business requirements.
|
modules to fit your business requirements.
|
||||||
</div>
|
</div>
|
||||||
@ -823,7 +835,12 @@ const LandingPage = () => {
|
|||||||
alt="hero elements"
|
alt="hero elements"
|
||||||
></img>
|
></img>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 text-start text-sm-center text-lg-start">
|
<div
|
||||||
|
className="col-lg-6 text-start text-sm-center text-lg-start p-5 rounded"
|
||||||
|
style={{
|
||||||
|
border: "1px solid #d5d5d5",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
{" "}
|
{" "}
|
||||||
<h4 className="text-start mb-1">
|
<h4 className="text-start mb-1">
|
||||||
@ -1164,7 +1181,7 @@ const LandingPage = () => {
|
|||||||
src="/img/brand/marco.png"
|
src="/img/brand/marco.png"
|
||||||
width="50"
|
width="50"
|
||||||
/>
|
/>
|
||||||
<span> Marco PMS</span>
|
<span> OnFieldWork.com</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
@ -1252,7 +1269,7 @@ const LandingPage = () => {
|
|||||||
<img src="/img/icons/apple-icon.png" alt="apple icon" />
|
<img src="/img/icons/apple-icon.png" alt="apple icon" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share"
|
href="https://play.google.com/store/apps/details?id=com.marcoonfieldwork.aiot&pcampaignid=web_share "
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const OrganizationPage = () => {
|
|||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[{ label: "Home", link: "/" }, { label: "Organizations" }]}
|
data={[{ label: "Home", link: "/" }, { label: "Organizations" }]}
|
||||||
/>
|
/>
|
||||||
<div className="card my-3 px-sm-2 px-0">
|
<div className="card my-3 px-sm-4 px-0">
|
||||||
<div className="card-body py-2 px-3">
|
<div className="card-body py-2 px-3">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col-6 d-flex ">
|
<div className="col-6 d-flex ">
|
||||||
@ -42,9 +42,13 @@ const OrganizationPage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OrganizationsList searchText={searchText} />
|
<div className="card page-min-h px-sm-4">
|
||||||
|
<OrganizationsList searchText={searchText} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -121,7 +121,7 @@ const TenantPage = () => {
|
|||||||
{ label: "Tenant", link: null },
|
{ label: "Tenant", link: null },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="card text-center my-4 p-5 pb-10">
|
<div className="card text-center my-4 p-md-5 px-1 py-3 pb-10">
|
||||||
{/* Super Tenant Actions */}
|
{/* Super Tenant Actions */}
|
||||||
{isSuperTenant && (
|
{isSuperTenant && (
|
||||||
<div className="p-0">
|
<div className="p-0">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { AuthWrapper } from "./AuthWrapper"
|
import { AuthWrapper } from "./AuthWrapper";
|
||||||
import "./page-auth.css";
|
import "./page-auth.css";
|
||||||
import AuthRepository from "../../repositories/AuthRepository";
|
import AuthRepository from "../../repositories/AuthRepository";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
@ -8,54 +8,76 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
||||||
const forgotPassSceham = z.object({
|
const forgotPassSceham = z.object({
|
||||||
email: z.string().trim().email(),
|
email: z.string().trim().email(),
|
||||||
})
|
});
|
||||||
|
|
||||||
const ForgotPasswordPage = () => {
|
const ForgotPasswordPage = () => {
|
||||||
|
const [loding, setLoading] = useState(false);
|
||||||
|
|
||||||
const [loding, setLoading] = useState(false)
|
const {
|
||||||
|
register,
|
||||||
const { register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
reset,
|
reset,
|
||||||
getValues } = useForm({
|
getValues,
|
||||||
resolver: zodResolver(forgotPassSceham),
|
} = useForm({
|
||||||
defaultValues: {
|
resolver: zodResolver(forgotPassSceham),
|
||||||
email: ""
|
defaultValues: {
|
||||||
}
|
email: "",
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
const response = await AuthRepository.forgotPassword(data)
|
const response = await AuthRepository.forgotPassword(data);
|
||||||
if (response.data && response.success)
|
if (response.data && response.success)
|
||||||
showToast("verification email has been sent to your registered email address", "success")
|
showToast(
|
||||||
reset()
|
"verification email has been sent to your registered email address",
|
||||||
setLoading(false)
|
"success"
|
||||||
|
);
|
||||||
|
reset();
|
||||||
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reset()
|
reset();
|
||||||
if (err.response.status === 404) {
|
if (err.response.status === 404) {
|
||||||
showToast("verification email has been sent to your registered email address", "success")
|
showToast(
|
||||||
|
"verification email has been sent to your registered email address",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showToast("Something wrong", "error")
|
showToast("Something wrong", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
||||||
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
|
<div className="w-100 m-auto" style={{ maxWidth: 420 }}>
|
||||||
<h4 className="mb-2">Forgot Password? 🔒</h4>
|
<div className="d-flex align-items-center justify-content-center ">
|
||||||
|
<img src="/img/brand/marco.png" width="70" />
|
||||||
|
<Link aria-label="Go to Home Page" to="/">
|
||||||
|
<span class="app-brand-logo ">
|
||||||
|
<span class="text-blue fs-4">OnField</span>
|
||||||
|
<span className="text-green fs-4">Work</span>
|
||||||
|
<span class="text-dark fs-4">.com</span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2 mt-5 ">Forgot Password? </h5>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
Enter your email and we'll send you instructions to reset your password
|
Enter your email and we'll send you instructions to reset your
|
||||||
|
password
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="formAuthentication" className="mb-3" onSubmit={handleSubmit(onSubmit)}>
|
<form
|
||||||
|
id="formAuthentication"
|
||||||
|
className="mb-3"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
<div className="mb-3 text-start">
|
<div className="mb-3 text-start">
|
||||||
<label htmlFor="email" className="form-label">
|
<label htmlFor="email" className="form-label">
|
||||||
Email
|
Email
|
||||||
@ -78,26 +100,28 @@ const ForgotPasswordPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button aria-label="Click me" className="btn btn-primary d-grid w-100">
|
<button
|
||||||
|
aria-label="Click me"
|
||||||
|
className="btn btn-primary d-grid w-100"
|
||||||
|
>
|
||||||
{loding ? "Please Wait..." : "Send Reset Link"}
|
{loding ? "Please Wait..." : "Send Reset Link"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Link
|
<Link
|
||||||
aria-label="Go to Login Page"
|
aria-label="Go to Login Page"
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="d-flex align-items-center justify-content-center"
|
className="d-flex align-items-center justify-content-center"
|
||||||
>
|
>
|
||||||
<i className="bx bx-chevron-left scaleX-n1-rtl bx-sm"></i>
|
<i className="bx bx-chevron-left scaleX-n1-rtl bx-sm"></i>
|
||||||
Back to login
|
Back to login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Text */}
|
{/* Footer Text */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ForgotPasswordPage;
|
export default ForgotPasswordPage;
|
||||||
|
|||||||
@ -45,11 +45,11 @@ const LoginPage = () => {
|
|||||||
if (data.rememberMe) {
|
if (data.rememberMe) {
|
||||||
localStorage.setItem("jwtToken", response.data.token);
|
localStorage.setItem("jwtToken", response.data.token);
|
||||||
localStorage.setItem("refreshToken", response.data.refreshToken);
|
localStorage.setItem("refreshToken", response.data.refreshToken);
|
||||||
removeSession("session")
|
removeSession("session");
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.setItem("jwtToken", response.data.token);
|
sessionStorage.setItem("jwtToken", response.data.token);
|
||||||
sessionStorage.setItem("refreshToken", response.data.refreshToken);
|
sessionStorage.setItem("refreshToken", response.data.refreshToken);
|
||||||
removeSession("local")
|
removeSession("local");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
navigate("/auth/switch/org");
|
navigate("/auth/switch/org");
|
||||||
@ -78,25 +78,35 @@ const LoginPage = () => {
|
|||||||
}, [IsLoginWithOTP]);
|
}, [IsLoginWithOTP]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token =
|
const token =
|
||||||
localStorage.getItem("jwtToken") ||
|
localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken");
|
||||||
sessionStorage.getItem("jwtToken");
|
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
navigate("/dashboard", { replace: true });
|
navigate("/dashboard", { replace: true });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
||||||
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
|
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
|
||||||
<h4 className="mb-2">Welcome to PMS!</h4>
|
<Link aria-label="Go to Home Page" to="/">
|
||||||
|
<span class="app-brand-logo rounded-circle app-brand-logo-border">
|
||||||
|
<img src="/img/brand/marco.png" width="70" />
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-dark fs-5">Welcome to</span> <br />
|
||||||
|
<h4 className="mb-2 ">
|
||||||
|
{" "}
|
||||||
|
<span class="text-blue ms-1">OnField</span>
|
||||||
|
<span className="text-green">Work</span>
|
||||||
|
<span class="text-dark">.com</span>
|
||||||
|
</h4>
|
||||||
|
</Link>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
{IsLoginWithOTP
|
{IsLoginWithOTP
|
||||||
? "Enter your email to receive a one-time password (OTP)."
|
? "Enter your email to receive a one-time password (OTP)."
|
||||||
: "Please sign in to your account and start the adventure"}
|
: "Please sign in to your account and start the adventure"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="formAuthentication" onSubmit={handleSubmit(onSubmit)}>
|
<form id="formAuthentication" onSubmit={handleSubmit(onSubmit)}>
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="mb-3 text-start">
|
<div className="mb-3 text-start">
|
||||||
@ -219,7 +229,6 @@ const LoginPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Footer Text */}
|
{/* Footer Text */}
|
||||||
{!IsLoginWithOTP ? (
|
{!IsLoginWithOTP ? (
|
||||||
<p className="text-center mt-3">
|
<p className="text-center mt-3">
|
||||||
|
|||||||
@ -37,33 +37,36 @@ const registerSchema = z.object({
|
|||||||
const RegisterPage = () => {
|
const RegisterPage = () => {
|
||||||
const [registered, setRegristered] = useState(false);
|
const [registered, setRegristered] = useState(false);
|
||||||
const [industries, setIndustries] = useState([]);
|
const [industries, setIndustries] = useState([]);
|
||||||
const [Loading,setLoading] = useState(false)
|
const [Loading, setLoading] = useState(false);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },reset
|
formState: { errors },
|
||||||
|
reset,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(registerSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
const response = await MarketRepository.requestDemo(data);
|
const response = await MarketRepository.requestDemo(data);
|
||||||
showToast("Your request has been sent successfully. Please stay in touch!");
|
showToast(
|
||||||
|
"Your request has been sent successfully. Please stay in touch!"
|
||||||
|
);
|
||||||
setRegristered(true);
|
setRegristered(true);
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
reset()
|
reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message, "error");
|
showToast(error.message, "error");
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchIndustries();
|
fetchIndustries();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { }, [industries]);
|
useEffect(() => {}, [industries]);
|
||||||
|
|
||||||
const fetchIndustries = async () => {
|
const fetchIndustries = async () => {
|
||||||
try {
|
try {
|
||||||
@ -76,11 +79,20 @@ const RegisterPage = () => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
<div className="col-12 col-lg-5 col-xl-4 d-flex align-items-center p-4 p-sm-5 bg-gray-60">
|
||||||
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
|
<div className="w-100" style={{ maxWidth: 420, margin: "0 auto" }}>
|
||||||
|
<div className="d-flex align-items-center justify-content-center ">
|
||||||
<h4 className="mb-2">Adventure starts here </h4>
|
<img src="/img/brand/marco.png" width="50" />
|
||||||
|
<Link aria-label="Go to Home Page" to="/">
|
||||||
|
<span class="app-brand-logo ">
|
||||||
|
<span class="text-blue fs-4">OnField</span>
|
||||||
|
<span className="text-green fs-4">Work</span>
|
||||||
|
<span class="text-dark fs-4">.com</span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2">Adventure starts here </h5>
|
||||||
<p className="mb-3">Make your app management easy and fun!</p>
|
<p className="mb-3">Make your app management easy and fun!</p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@ -88,65 +100,64 @@ const RegisterPage = () => {
|
|||||||
className="mb-2"
|
className="mb-2"
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-sm-6 mb-2 text-start">
|
<div className="col-12 col-sm-6 mb-2 text-start">
|
||||||
<label htmlFor="organizatioinName" className="form-label">
|
<label htmlFor="organizatioinName" className="form-label">
|
||||||
Organization Name
|
Organization Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
id="organizatioinName"
|
id="organizatioinName"
|
||||||
{...register("organizatioinName")}
|
{...register("organizatioinName")}
|
||||||
name="organizatioinName"
|
name="organizatioinName"
|
||||||
placeholder="Enter your Organization Name"
|
placeholder="Enter your Organization Name"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{errors.organizatioinName && (
|
{errors.organizatioinName && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{errors.organizatioinName.message}
|
{errors.organizatioinName.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-6 mb-2 text-start">
|
<div className="col-12 col-sm-6 mb-2 text-start">
|
||||||
<label htmlFor="email" className="form-label">
|
<label htmlFor="email" className="form-label">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{errors.email.message}
|
{errors.email.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 form-password-toggle text-start">
|
<div className="mb-2 form-password-toggle text-start">
|
||||||
<label className="form-label" htmlFor="contactperson">
|
<label className="form-label" htmlFor="contactperson">
|
||||||
Contact Person
|
Contact Person
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="contactperson"
|
id="contactperson"
|
||||||
{...register("contactPerson")}
|
{...register("contactPerson")}
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
name="contactPerson"
|
name="contactPerson"
|
||||||
placeholder="Contact Person"
|
placeholder="Contact Person"
|
||||||
aria-describedby="contactperson"
|
aria-describedby="contactperson"
|
||||||
/>
|
/>
|
||||||
{errors.contactPerson && (
|
{errors.contactPerson && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
@ -160,15 +171,15 @@ const RegisterPage = () => {
|
|||||||
<label className="form-label" htmlFor="contactnumber">
|
<label className="form-label" htmlFor="contactnumber">
|
||||||
Contact Number
|
Contact Number
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="contactnumber"
|
id="contactnumber"
|
||||||
{...register("contactNumber")}
|
{...register("contactNumber")}
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
name="contactNumber"
|
name="contactNumber"
|
||||||
placeholder="Contact Number"
|
placeholder="Contact Number"
|
||||||
aria-describedby="contactnumber"
|
aria-describedby="contactnumber"
|
||||||
/>
|
/>
|
||||||
{errors.contactNumber && (
|
{errors.contactNumber && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
@ -182,14 +193,14 @@ const RegisterPage = () => {
|
|||||||
<label className="form-label" htmlFor="contactnumber">
|
<label className="form-label" htmlFor="contactnumber">
|
||||||
About Organization
|
About Organization
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="about"
|
id="about"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="About..."
|
placeholder="About..."
|
||||||
aria-label="about"
|
aria-label="about"
|
||||||
aria-describedby="about"
|
aria-describedby="about"
|
||||||
{...register("about")}
|
{...register("about")}
|
||||||
></textarea>
|
></textarea>
|
||||||
{errors.about && (
|
{errors.about && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
@ -203,20 +214,20 @@ const RegisterPage = () => {
|
|||||||
<label className="form-label" htmlFor="oragnizationSize">
|
<label className="form-label" htmlFor="oragnizationSize">
|
||||||
Organization Size
|
Organization Size
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
id="oragnizationSize"
|
id="oragnizationSize"
|
||||||
name="oragnizationSize"
|
name="oragnizationSize"
|
||||||
{...register("oragnizationSize")}
|
{...register("oragnizationSize")}
|
||||||
aria-label="Default select example"
|
aria-label="Default select example"
|
||||||
>
|
>
|
||||||
<option value="">Number of Employees</option>
|
<option value="">Number of Employees</option>
|
||||||
<option value="1-10">1-10</option>
|
<option value="1-10">1-10</option>
|
||||||
<option value="10-50">10-50</option>
|
<option value="10-50">10-50</option>
|
||||||
<option value="50-100">50-100</option>
|
<option value="50-100">50-100</option>
|
||||||
<option value="100-200">100-200</option>
|
<option value="100-200">100-200</option>
|
||||||
<option value="more than 200">more than 200</option>
|
<option value="more than 200">more than 200</option>
|
||||||
</select>
|
</select>
|
||||||
{errors.oragnizationSize && (
|
{errors.oragnizationSize && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
@ -230,24 +241,24 @@ const RegisterPage = () => {
|
|||||||
<label className="form-label" htmlFor="industryId">
|
<label className="form-label" htmlFor="industryId">
|
||||||
Industry
|
Industry
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
id="industryId"
|
id="industryId"
|
||||||
name="industryId"
|
name="industryId"
|
||||||
{...register("industryId")}
|
{...register("industryId")}
|
||||||
aria-label="Default select example"
|
aria-label="Default select example"
|
||||||
>
|
>
|
||||||
<option value="">Select Industry</option>
|
<option value="">Select Industry</option>
|
||||||
{industries.length > 0 ? (
|
{industries.length > 0 ? (
|
||||||
industries.map((item) => (
|
industries.map((item) => (
|
||||||
<option value={item.id} key={item.id}>
|
<option value={item.id} key={item.id}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<option disabled>Loading industries...</option>
|
<option disabled>Loading industries...</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
{errors.industryId && (
|
{errors.industryId && (
|
||||||
<div
|
<div
|
||||||
className="danger-text text-start"
|
className="danger-text text-start"
|
||||||
@ -276,7 +287,6 @@ const RegisterPage = () => {
|
|||||||
privacy policy & terms
|
privacy policy & terms
|
||||||
</Link>
|
</Link>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{errors.terms && (
|
{errors.terms && (
|
||||||
<div
|
<div
|
||||||
@ -291,7 +301,7 @@ const RegisterPage = () => {
|
|||||||
aria-label="Click me "
|
aria-label="Click me "
|
||||||
className="btn btn-primary d-grid w-100"
|
className="btn btn-primary d-grid w-100"
|
||||||
>
|
>
|
||||||
{Loading ? "Please Wait..." :" Request Demo"}
|
{Loading ? "Please Wait..." : " Request Demo"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -313,4 +323,4 @@ const RegisterPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RegisterPage;
|
export default RegisterPage;
|
||||||
|
|||||||
@ -8,35 +8,43 @@ const TenantSelectionPage = () => {
|
|||||||
const [pendingTenant, setPendingTenant] = useState(null);
|
const [pendingTenant, setPendingTenant] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useTenants();
|
const { data, isLoading, isError } = useTenants();
|
||||||
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
|
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
});
|
});
|
||||||
const handleTenantselection = (tenantId) => {
|
const { mutate: handleLogout, isPending: isLogouting } = useLogout();
|
||||||
|
|
||||||
|
const handleTenantSelection = (tenantId) => {
|
||||||
setPendingTenant(tenantId);
|
setPendingTenant(tenantId);
|
||||||
localStorage.setItem("ctnt", tenantId);
|
localStorage.setItem("ctnt", tenantId);
|
||||||
chooseTenant(tenantId);
|
chooseTenant(tenantId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {mutate:handleLogout,isPending:isLogouting} = useLogout()
|
|
||||||
|
|
||||||
|
|
||||||
|
// Auto-select if already stored
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem("ctnt")) {
|
const storedTenant = localStorage.getItem("ctnt");
|
||||||
chooseTenant(localStorage.getItem("ctnt"))
|
if (storedTenant) {
|
||||||
|
chooseTenant(storedTenant);
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, []);
|
||||||
|
|
||||||
|
// Auto-select if only one tenant
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && data?.data?.length === 1) {
|
if (!isLoading && data?.data?.length === 1) {
|
||||||
const tenant = data.data[0];
|
const tenant = data.data[0];
|
||||||
handleTenantselection(tenant.id);
|
handleTenantSelection(tenant.id);
|
||||||
}
|
}
|
||||||
}, [isLoading, data]);
|
}, [isLoading, data]);
|
||||||
|
|
||||||
if (isLoading) return <Loader />;
|
// Show loader if:
|
||||||
|
// - initial loading
|
||||||
if (isLoading) {
|
// - only one tenant (auto-selecting)
|
||||||
|
// - user manually selecting
|
||||||
|
if (
|
||||||
|
isLoading ||
|
||||||
|
isPending ||
|
||||||
|
(data?.data?.length === 1 && pendingTenant !== null)
|
||||||
|
) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +56,6 @@ const TenantSelectionPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
@ -65,20 +72,24 @@ const TenantSelectionPage = () => {
|
|||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<p className="fs-4 fw-bold mb-1">Welcome</p>
|
<p className="fs-4 fw-bold mb-1">Welcome</p>
|
||||||
<p className="fs-6 fs-md-5">
|
<p className="fs-6 fs-md-5">
|
||||||
Please select which dashboard you want to explore!!!
|
Please select which dashboard you want to explore!
|
||||||
</p>
|
</p>
|
||||||
<div onClick={()=>handleLogout()}>
|
<div onClick={() => handleLogout()}>
|
||||||
{isLogouting ? "Please Wait...":<span className="fs-6 fw-semibold cursor-pointer text-decoration-underline"><i className='bx bx-log-out'></i>SignOut</span>}
|
{isLogouting ? (
|
||||||
|
"Please Wait..."
|
||||||
|
) : (
|
||||||
|
<span className="fs-6 fw-semibold cursor-pointer text-decoration-underline">
|
||||||
|
<i className="bx bx-log-out"></i> Sign Out
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card Section */}
|
{/* Tenant Cards */}
|
||||||
<div className="row justify-content-center g-4 ">
|
<div className="row justify-content-center g-4">
|
||||||
{data?.data.map((tenant) => (
|
{data?.data.map((tenant) => (
|
||||||
<div key={tenant.id} className="col-12 col-md-10 col-lg-8">
|
<div key={tenant.id} className="col-12 col-md-10 col-lg-8">
|
||||||
<div className="d-flex flex-column flex-md-row gap-4 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100">
|
<div className="d-flex flex-column flex-md-row gap-4 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100">
|
||||||
|
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="flex-shrink-0 text-center">
|
<div className="flex-shrink-0 text-center">
|
||||||
<img
|
<img
|
||||||
@ -95,12 +106,10 @@ const TenantSelectionPage = () => {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="d-flex flex-column text-start gap-2 w-100">
|
<div className="d-flex flex-column text-start gap-2 w-100">
|
||||||
{/* Title */}
|
|
||||||
<p className="fs-5 fs-md-4 text-dark fw-semibold mb-1">
|
<p className="fs-5 fs-md-4 text-dark fw-semibold mb-1">
|
||||||
{tenant?.name}
|
{tenant?.name}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Industry */}
|
|
||||||
<div className="d-flex flex-wrap gap-2 align-items-center">
|
<div className="d-flex flex-wrap gap-2 align-items-center">
|
||||||
<p className="fw-semibold m-0">Industry:</p>
|
<p className="fw-semibold m-0">Industry:</p>
|
||||||
<p className="m-0 text-muted">
|
<p className="m-0 text-muted">
|
||||||
@ -108,21 +117,19 @@ const TenantSelectionPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
{tenant?.description && (
|
{tenant?.description && (
|
||||||
<p className="text-start text-wrap text-muted small m-0">
|
<p className="text-start text-wrap text-muted small m-0">
|
||||||
{tenant?.description}
|
{tenant?.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Button */}
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-sm mt-2 align-self-start"
|
className="btn btn-primary btn-sm mt-2 align-self-start"
|
||||||
onClick={() => handleTenantselection(tenant?.id)}
|
onClick={() => handleTenantSelection(tenant?.id)}
|
||||||
disabled={pendingTenant === tenant.id && isPending}
|
disabled={pendingTenant === tenant.id && isPending}
|
||||||
>
|
>
|
||||||
{isPending && pendingTenant === tenant.id
|
{pendingTenant === tenant.id && isPending
|
||||||
? "Please Wait.."
|
? "Please Wait..."
|
||||||
: "Go To Dashboard"}
|
: "Go To Dashboard"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -131,8 +138,8 @@ const TenantSelectionPage = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TenantSelectionPage;
|
export default TenantSelectionPage;
|
||||||
|
|
||||||
|
|||||||
@ -82,18 +82,29 @@ const CollectionPage = () => {
|
|||||||
const handleMarkedPayment = (payload) => {
|
const handleMarkedPayment = (payload) => {
|
||||||
MarkedReceived(payload);
|
MarkedReceived(payload);
|
||||||
};
|
};
|
||||||
if (isAdmin === undefined ||
|
if (
|
||||||
canAddPayment === undefined ||
|
isAdmin === undefined ||
|
||||||
canEditCollection === undefined ||
|
canAddPayment === undefined ||
|
||||||
canViewCollection === undefined ||
|
canEditCollection === undefined ||
|
||||||
|
canViewCollection === undefined ||
|
||||||
canCreate === undefined
|
canCreate === undefined
|
||||||
) {
|
) {
|
||||||
return <div className="text-center py-5">Checking access...</div>;
|
return <div className="text-center py-5">Checking access...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin && !canAddPayment && !canEditCollection && !canViewCollection && !canCreate) {
|
if (
|
||||||
return <AccessDenied data={[{ label: "Home", link: "/" }, { label: "Collection" }]} />;
|
!isAdmin &&
|
||||||
}
|
!canAddPayment &&
|
||||||
|
!canEditCollection &&
|
||||||
|
!canViewCollection &&
|
||||||
|
!canCreate
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<AccessDenied
|
||||||
|
data={[{ label: "Home", link: "/" }, { label: "Collection" }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<CollectionContext.Provider value={contextMassager}>
|
<CollectionContext.Provider value={contextMassager}>
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
@ -127,28 +138,31 @@ if (!isAdmin && !canAddPayment && !canEditCollection && !canViewCollection && !c
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6 d-flex justify-content-end gap-4">
|
<div className="col-12 col-md-6 d-flex justify-content-between justify-content-md-end gap-4">
|
||||||
<div className=" w-md-auto">
|
<div className="w-md-auto">
|
||||||
{" "}
|
{" "}
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
placeholder="search Collection"
|
placeholder="Search Collection"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ (canCreate || isAdmin) && (
|
{(canCreate || isAdmin) && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCollection({ isOpen: true, invoiceId: null })}
|
onClick={() =>
|
||||||
>
|
setCollection({ isOpen: true, invoiceId: null })
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
}
|
||||||
<span className="d-none d-md-inline-block">Add New Collection</span>
|
>
|
||||||
</button>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
)}
|
<span className="d-none d-md-inline-block">
|
||||||
|
Add New Collection
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import usePagination from "../../hooks/usePagination";
|
|||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import Pagination from "../../components/common/Pagination";
|
import Pagination from "../../components/common/Pagination";
|
||||||
|
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
|
||||||
|
|
||||||
const EmployeeList = () => {
|
const EmployeeList = () => {
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
@ -134,26 +135,11 @@ const EmployeeList = () => {
|
|||||||
|
|
||||||
const tableRef = useRef(null);
|
const tableRef = useRef(null);
|
||||||
const handleExport = (type) => {
|
const handleExport = (type) => {
|
||||||
if (!currentItems || currentItems.length === 0) return;
|
handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "csv":
|
|
||||||
exportToCSV(currentItems, "employees");
|
|
||||||
break;
|
|
||||||
case "excel":
|
|
||||||
exportToExcel(currentItems, "employees");
|
|
||||||
break;
|
|
||||||
case "pdf":
|
|
||||||
exportToPDF(currentItems, "employees");
|
|
||||||
break;
|
|
||||||
case "print":
|
|
||||||
printTable(tableRef.current);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleAllEmployeesToggle = (e) => {
|
const handleAllEmployeesToggle = (e) => {
|
||||||
const isChecked = e.target.checked;
|
const isChecked = e.target.checked;
|
||||||
setShowInactive(false);
|
setShowInactive(false);
|
||||||
@ -176,12 +162,10 @@ const EmployeeList = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && Array.isArray(employees)) {
|
if (!loading && Array.isArray(employees)) {
|
||||||
const sorted = [...employees].sort((a, b) => {
|
const sorted = [...employees].sort((a, b) => {
|
||||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${
|
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
|
||||||
a.lastName || ""
|
}`.toLowerCase();
|
||||||
}`.toLowerCase();
|
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
|
||||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${
|
}`.toLowerCase();
|
||||||
b.lastName || ""
|
|
||||||
}`.toLowerCase();
|
|
||||||
return nameA?.localeCompare(nameB);
|
return nameA?.localeCompare(nameB);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,9 +242,8 @@ const EmployeeList = () => {
|
|||||||
? "Suspend Employee"
|
? "Suspend Employee"
|
||||||
: "Reactivate Employee"
|
: "Reactivate Employee"
|
||||||
}
|
}
|
||||||
message={`Are you sure you want to ${
|
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
||||||
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
} this employee?`}
|
||||||
} this employee?`}
|
|
||||||
onSubmit={(id) =>
|
onSubmit={(id) =>
|
||||||
suspendEmployee({
|
suspendEmployee({
|
||||||
employeeId: id,
|
employeeId: id,
|
||||||
@ -309,7 +292,7 @@ const EmployeeList = () => {
|
|||||||
className="form-check-label ms-0"
|
className="form-check-label ms-0"
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
htmlFor="inactiveEmployeesCheckbox"
|
||||||
>
|
>
|
||||||
Show Inactive Employees
|
In-active Employees
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -471,9 +454,8 @@ const EmployeeList = () => {
|
|||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className={`sorting_disabled ${
|
className={`sorting_disabled ${!Manage_Employee && "d-none"
|
||||||
!Manage_Employee && "d-none"
|
}`}
|
||||||
}`}
|
|
||||||
rowSpan="1"
|
rowSpan="1"
|
||||||
colSpan="1"
|
colSpan="1"
|
||||||
style={{ width: "50px" }}
|
style={{ width: "50px" }}
|
||||||
@ -493,20 +475,20 @@ const EmployeeList = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
displayData?.length === 0 &&
|
displayData?.length === 0 &&
|
||||||
(!searchText ) ? (
|
(!searchText) ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="border-0 py-3">
|
<td colSpan={8} className="border-0 py-3">
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
No Data Found
|
No Data Found
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
displayData?.length === 0 &&
|
displayData?.length === 0 &&
|
||||||
(searchText ) ? (
|
(searchText) ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="border-0 py-3">
|
<td colSpan={8} className="border-0 py-3">
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
@ -542,18 +524,17 @@ const EmployeeList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-start d-none d-sm-table-cell">
|
<td className="text-start d-none d-sm-table-cell">
|
||||||
{item.email ? (
|
{item.email ? (
|
||||||
<span className="text-truncate">
|
<span className="text-truncate">
|
||||||
<i className="bx bxs-envelope text-primary me-2"></i>
|
<i className="bx bxs-envelope text-primary me-2"></i>
|
||||||
{item.email}
|
{item.email}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-truncate text-italic">
|
<span className="d-block text-start text-muted fst-italic">NA</span>
|
||||||
-
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="text-start d-none d-sm-table-cell">
|
<td className="text-start d-none d-sm-table-cell">
|
||||||
<span className="text-truncate">
|
<span className="text-truncate">
|
||||||
<i className="bx bxs-phone-call text-primary me-2"></i>
|
<i className="bx bxs-phone-call text-primary me-2"></i>
|
||||||
@ -567,9 +548,14 @@ const EmployeeList = () => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className=" d-none d-md-table-cell">
|
<td className="d-none d-md-table-cell">
|
||||||
{moment(item.joiningDate)?.format("DD-MMM-YYYY")}
|
{item.joiningDate ? (
|
||||||
|
moment(item.joiningDate).format("DD-MMM-YYYY")
|
||||||
|
) : (
|
||||||
|
<span className="d-block text-center text-muted fst-italic">NA</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{showInactive ? (
|
{showInactive ? (
|
||||||
<span
|
<span
|
||||||
@ -663,17 +649,16 @@ const EmployeeList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{displayData?.length > 0 && (
|
</div>
|
||||||
|
{displayData?.length > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
onPageChange={paginate}
|
onPageChange={paginate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|||||||
@ -186,8 +186,8 @@ const MasterPage = () => {
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-9 d-flex justify-content-end align-items-center gap-2 mt-2 mt-md-0">
|
<div className="col-12 col-md-9 d-flex justify-content-between justify-content-md-end align-items-center gap-2 mt-2 mt-md-0">
|
||||||
<div className="col-6 col-md-3">
|
<div className="col-8 col-md-3">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
|
|||||||
@ -77,7 +77,7 @@ const ProjectDetails = () => {
|
|||||||
<AboutProject />
|
<AboutProject />
|
||||||
<ProjectOverview project={projectId} />
|
<ProjectOverview project={projectId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-8 col-md-7 mt-5">
|
<div className="col-lg-8 col-md-7 mt-2">
|
||||||
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
|
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<AttendanceOverview />
|
<AttendanceOverview />
|
||||||
|
|||||||
@ -96,8 +96,8 @@ const ProjectPage = () => {
|
|||||||
}, [data, isLoading, selectedStatuses]);
|
}, [data, isLoading, selectedStatuses]);
|
||||||
|
|
||||||
|
|
||||||
if(isLoading) return <div className="page-min-h"><Loader/></div>
|
if (isLoading) return <div className="page-min-h"><Loader /></div>
|
||||||
if(isError) return <div className="page-min-h d-flex justify-content-center align-items-center"><p>{error.message}</p></div>
|
if (isError) return <div className="page-min-h d-flex justify-content-center align-items-center"><p>{error.message}</p></div>
|
||||||
return (
|
return (
|
||||||
<ProjectContext.Provider value={contextDispatcher}>
|
<ProjectContext.Provider value={contextDispatcher}>
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
@ -128,9 +128,8 @@ const ProjectPage = () => {
|
|||||||
<div className="d-flex gap-2 mb-2">
|
<div className="d-flex gap-2 mb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${
|
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
|
||||||
!listView ? "btn-primary" : "btn-outline-primary"
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setListView(false)}
|
onClick={() => setListView(false)}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-custom-class="tooltip"
|
data-bs-custom-class="tooltip"
|
||||||
@ -140,9 +139,8 @@ const ProjectPage = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${
|
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
|
||||||
listView ? "btn-primary" : "btn-outline-primary"
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setListView(true)}
|
onClick={() => setListView(true)}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-custom-class="tooltip"
|
data-bs-custom-class="tooltip"
|
||||||
@ -180,20 +178,22 @@ const ProjectPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{HasManageProject && (
|
||||||
{HasManageProject && ( <button
|
<div className="ms-auto">
|
||||||
className="btn btn-sm btn-primary"
|
<button
|
||||||
type="button"
|
className="btn btn-sm btn-primary"
|
||||||
onClick={() =>
|
type="button"
|
||||||
setMangeProject({ isOpen: true, Project: null })
|
onClick={() =>
|
||||||
}
|
setMangeProject({ isOpen: true, Project: null })
|
||||||
>
|
}
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
>
|
||||||
<span className="d-none d-md-inline-block">
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Add New Project
|
<span className="d-none d-md-inline-block">
|
||||||
</span>
|
Add New Project
|
||||||
</button>)}
|
</span>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import { api } from "../utils/axiosClient";
|
|||||||
const GlobalRepository = {
|
const GlobalRepository = {
|
||||||
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
|
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
|
||||||
let params;
|
let params;
|
||||||
if(projectId == null){
|
if (projectId == null) {
|
||||||
params = new URLSearchParams({
|
params = new URLSearchParams({
|
||||||
days: days.toString(),
|
days: days.toString(),
|
||||||
FromDate,
|
FromDate,
|
||||||
});
|
});
|
||||||
}else{
|
} else {
|
||||||
params = new URLSearchParams({
|
params = new URLSearchParams({
|
||||||
days: days.toString(),
|
days: days.toString(),
|
||||||
FromDate,
|
FromDate,
|
||||||
@ -19,30 +19,72 @@ const GlobalRepository = {
|
|||||||
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
|
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
getDashboardAttendanceData: ( date,projectId ) => {
|
getDashboardAttendanceData: (date, projectId) => {
|
||||||
|
|
||||||
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
|
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
|
||||||
},
|
},
|
||||||
getDashboardProjectsCardData: () => {
|
getDashboardProjectsCardData: () => {
|
||||||
return api.get(`/api/Dashboard/projects`);
|
return api.get(`/api/Dashboard/projects`);
|
||||||
},
|
},
|
||||||
|
|
||||||
getDashboardTeamsCardData: (projectId) => {
|
getDashboardTeamsCardData: (projectId) => {
|
||||||
const url = projectId
|
const url = projectId
|
||||||
? `/api/Dashboard/teams?projectId=${projectId}`
|
? `/api/Dashboard/teams?projectId=${projectId}`
|
||||||
: `/api/Dashboard/teams`;
|
: `/api/Dashboard/teams`;
|
||||||
return api.get(url);
|
return api.get(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
getDashboardTasksCardData: (projectId) => {
|
getDashboardTasksCardData: (projectId) => {
|
||||||
const url = projectId
|
const url = projectId
|
||||||
? `/api/Dashboard/tasks?projectId=${projectId}`
|
? `/api/Dashboard/tasks?projectId=${projectId}`
|
||||||
: `/api/Dashboard/tasks`;
|
: `/api/Dashboard/tasks`;
|
||||||
return api.get(url);
|
return api.get(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getExpenseData: (projectId, startDate, endDate) => {
|
||||||
|
let url = `api/Dashboard/expense/type`
|
||||||
|
const queryParams = [];
|
||||||
|
if (projectId) {
|
||||||
|
queryParams.push(`projectId=${projectId}`);
|
||||||
|
}
|
||||||
|
if (startDate) {
|
||||||
|
queryParams.push(`startDate=${startDate}`);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
queryParams.push(`endDate=${endDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
url += `?${queryParams.join("&")}`;
|
||||||
|
}
|
||||||
|
return api.get(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
getExpenseStatus: (projectId) => api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}` : ""}`),
|
||||||
|
|
||||||
|
getExpenseDataByProject: (projectId, categoryId, months) => {
|
||||||
|
let url = `api/Dashboard/expense/monthly`
|
||||||
|
const queryParams = [];
|
||||||
|
if (projectId) {
|
||||||
|
queryParams.push(`projectId=${projectId}`);
|
||||||
|
}
|
||||||
|
if (categoryId) {
|
||||||
|
queryParams.push(`categoryId=${categoryId}`);
|
||||||
|
}
|
||||||
|
if (months) {
|
||||||
|
queryParams.push(`months=${months}`);
|
||||||
|
}
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
url += `?${queryParams.join("&")}`;
|
||||||
|
}
|
||||||
|
return api.get(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttendanceOverview: (projectId, days) => api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`)
|
||||||
|
|
||||||
|
|
||||||
getAttendanceOverview:(projectId,days)=>api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`)
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default GlobalRepository;
|
export default GlobalRepository;
|
||||||
|
|||||||
@ -104,6 +104,7 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/activities/task", element: <TaskPlannng /> },
|
{ path: "/activities/task", element: <TaskPlannng /> },
|
||||||
{ path: "/activities/reports", element: <Reports /> },
|
{ path: "/activities/reports", element: <Reports /> },
|
||||||
{ path: "/gallary", element: <ImageGalleryPage /> },
|
{ path: "/gallary", element: <ImageGalleryPage /> },
|
||||||
|
{ path: "/expenses/:status?/:project?", element: <ExpensePage /> },
|
||||||
{ path: "/expenses", element: <ExpensePage /> },
|
{ path: "/expenses", element: <ExpensePage /> },
|
||||||
{ path: "/collection", element: <CollectionPage /> },
|
{ path: "/collection", element: <CollectionPage /> },
|
||||||
{ path: "/masters", element: <MasterPage /> },
|
{ path: "/masters", element: <MasterPage /> },
|
||||||
|
|||||||
@ -154,4 +154,13 @@ export const PROJECT_STATUS = [
|
|||||||
label: "Completed",
|
label: "Completed",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const EXPENSE_STATUS = {
|
||||||
|
daft:"297e0d8f-f668-41b5-bfea-e03b354251c8",
|
||||||
|
review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7",
|
||||||
|
payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27",
|
||||||
|
approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8",
|
||||||
|
process_pending:"61578360-3a49-4c34-8604-7b35a3787b95"
|
||||||
|
|
||||||
|
}
|
||||||
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const convertShortTime = (dateString) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const timeElapsed = (checkInTime, timeElapsedInHours) => {
|
export const timeElapsed = (checkInTime, timeElapsedInHours) => {
|
||||||
const checkInDate = new Date( checkInTime.split( "T" )[ 0 ] );
|
const checkInDate = new Date(checkInTime.split("T")[0]);
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export const checkIfCurrentDate = (dateString) => {
|
|||||||
return currentDate?.getTime() === inputDate?.getTime();
|
return currentDate?.getTime() === inputDate?.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatNumber = (num) => {
|
export const formatNumber = (num) => {
|
||||||
if (num == null || isNaN(num)) return "NA";
|
if (num == null || isNaN(num)) return "NA";
|
||||||
return Number.isInteger(num) ? num : num.toFixed(2);
|
return Number.isInteger(num) ? num : num.toFixed(2);
|
||||||
};
|
};
|
||||||
@ -84,15 +84,25 @@ export const formatUTCToLocalTime = (datetime, timeRequired = false) => {
|
|||||||
: moment.utc(datetime).local().format("DD MMM YYYY");
|
: moment.utc(datetime).local().format("DD MMM YYYY");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCompletionPercentage = (completedWork, plannedWork)=> {
|
export const getCompletionPercentage = (completedWork, plannedWork) => {
|
||||||
if (!plannedWork || plannedWork === 0) return 0;
|
if (!plannedWork || plannedWork === 0) return 0;
|
||||||
|
|
||||||
const percentage = (completedWork / plannedWork) * 100;
|
const percentage = (completedWork / plannedWork) * 100;
|
||||||
const clamped = Math.min(Math.max(percentage, 0), 100);
|
const clamped = Math.min(Math.max(percentage, 0), 100);
|
||||||
|
|
||||||
return clamped.toFixed(2);
|
return clamped.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTenantStatus =(statusId)=>{
|
export const formatDate_DayMonth = (monthName, year) => {
|
||||||
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
|
if (!monthName || !year) return "";
|
||||||
|
try {
|
||||||
|
const shortMonth = monthName.substring(0, 3);
|
||||||
|
return `${shortMonth} ${year}`;
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTenantStatus = (statusId) => {
|
||||||
|
return ActiveTenant === statusId ? " bg-label-success" : "bg-label-secondary"
|
||||||
}
|
}
|
||||||
@ -40,112 +40,57 @@ export const exportToExcel = (data, fileName = "data") => {
|
|||||||
* @param {Array} data - Array of objects to export
|
* @param {Array} data - Array of objects to export
|
||||||
* @param {string} fileName - File name for the PDF (optional)
|
* @param {string} fileName - File name for the PDF (optional)
|
||||||
*/
|
*/
|
||||||
export const exportToPDF = async (data, fileName = "data") => {
|
const sanitizeText = (text) => {
|
||||||
|
if (!text) return "";
|
||||||
|
// Replace all non-ASCII characters with "?" or remove them
|
||||||
|
return text.replace(/[^\x00-\x7F]/g, "?");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportToPDF = async (data, fileName = "data", columns = null, options = {}) => {
|
||||||
if (!data || data.length === 0) return;
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
// Create a new PDF document
|
|
||||||
const pdfDoc = await PDFDocument.create();
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
|
||||||
// Set up the font
|
// Default options
|
||||||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font
|
const {
|
||||||
|
columnWidths = [], // array of widths per column
|
||||||
// Calculate column widths dynamically based on data content
|
fontSizeHeader = 12,
|
||||||
const headers = Object.keys(data[0]);
|
fontSizeRow = 10,
|
||||||
const rows = data.map(item => headers.map(header => item[header] || ''));
|
rowHeight = 25,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const getMaxColumnWidth = (columnIndex) => {
|
const pageWidth = 1000;
|
||||||
let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12);
|
const pageHeight = 600;
|
||||||
rows.forEach(row => {
|
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
const cellText = row[columnIndex].toString();
|
const margin = 30;
|
||||||
maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10));
|
let y = pageHeight - margin;
|
||||||
|
|
||||||
|
const headers = columns || Object.keys(data[0]);
|
||||||
|
|
||||||
|
// Draw headers
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150);
|
||||||
|
page.drawText(header, { x, y, font, size: fontSizeHeader });
|
||||||
|
});
|
||||||
|
y -= rowHeight;
|
||||||
|
|
||||||
|
// Draw rows
|
||||||
|
data.forEach(row => {
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
const x = margin + (columnWidths[i] ? columnWidths.slice(0, i).reduce((a, b) => a + b, 0) : i * 150);
|
||||||
|
const text = row[header] || '';
|
||||||
|
page.drawText(text, { x, y, font, size: fontSizeRow });
|
||||||
});
|
});
|
||||||
return maxWidth + 10; // Padding for better spacing
|
y -= rowHeight;
|
||||||
};
|
|
||||||
|
|
||||||
const columnWidths = headers.map((_, index) => getMaxColumnWidth(index));
|
if (y < margin) {
|
||||||
const tableX = 30; // X-coordinate for the table start
|
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
const rowHeight = 20; // Height of each row (can be adjusted)
|
y = pageHeight - margin;
|
||||||
const maxPageHeight = 750; // Max available height for content (before a new page is added)
|
|
||||||
const pageMargin = 30; // Margin from the top of the page
|
|
||||||
|
|
||||||
let tableY = maxPageHeight; // Start Y position for the table
|
|
||||||
const maxPageWidth = 600; // Max available width for content (before a new page is added)
|
|
||||||
|
|
||||||
// Add the headers and rows to the page
|
|
||||||
const addHeadersToPage = (page, scaleFactor) => {
|
|
||||||
let xPosition = tableX;
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
page.drawText(header, {
|
|
||||||
x: xPosition,
|
|
||||||
y: tableY,
|
|
||||||
font,
|
|
||||||
size: 12 * scaleFactor, // Scale the header font size
|
|
||||||
color: rgb(0, 0, 0),
|
|
||||||
});
|
|
||||||
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
|
|
||||||
});
|
|
||||||
tableY -= rowHeight; // Move down after adding headers
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new page and reset the table position
|
|
||||||
const addNewPage = (scaleFactor) => {
|
|
||||||
const page = pdfDoc.addPage([600, 800]);
|
|
||||||
tableY = maxPageHeight; // Reset Y position for the new page
|
|
||||||
addHeadersToPage(page, scaleFactor); // Re-add headers to the new page
|
|
||||||
return page;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the first page and add headers
|
|
||||||
let page = pdfDoc.addPage([600, 800]);
|
|
||||||
|
|
||||||
// Check if the content fits within the page width, scale if necessary
|
|
||||||
const checkPageWidth = (row) => {
|
|
||||||
let totalWidth = columnWidths.reduce((acc, width) => acc + width, 0);
|
|
||||||
let scaleFactor = 1;
|
|
||||||
if (totalWidth > maxPageWidth) {
|
|
||||||
scaleFactor = maxPageWidth / totalWidth; // Scale down if necessary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return scaleFactor;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to check for page breaks when adding a new row
|
|
||||||
const checkPageBreak = () => {
|
|
||||||
if (tableY - rowHeight < pageMargin) {
|
|
||||||
page = addNewPage(scaleFactor); // Add a new page if there is no space for the next row
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add rows to the PDF with pagination and horizontal scaling
|
|
||||||
rows.forEach(row => {
|
|
||||||
checkPageBreak(); // Check for page break before adding each row
|
|
||||||
|
|
||||||
const scaleFactor = checkPageWidth(row); // Get the scaling factor for the row
|
|
||||||
|
|
||||||
// Add headers to the first page and each new page with the same scale factor
|
|
||||||
if (tableY === maxPageHeight) {
|
|
||||||
addHeadersToPage(page, scaleFactor); // Add headers only on the first page
|
|
||||||
}
|
|
||||||
|
|
||||||
let xPosition = tableX;
|
|
||||||
row.forEach((value, index) => {
|
|
||||||
page.drawText(value.toString(), {
|
|
||||||
x: xPosition,
|
|
||||||
y: tableY,
|
|
||||||
font,
|
|
||||||
size: 10 * scaleFactor, // Scale the font size
|
|
||||||
color: rgb(0, 0, 0),
|
|
||||||
});
|
|
||||||
xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling
|
|
||||||
});
|
|
||||||
|
|
||||||
tableY -= rowHeight; // Move down to the next row position
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize the document to bytes
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
// Trigger a download of the PDF
|
|
||||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob);
|
link.href = URL.createObjectURL(blob);
|
||||||
@ -153,15 +98,110 @@ export const exportToPDF = async (data, fileName = "data") => {
|
|||||||
link.click();
|
link.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export JSON data to PDF in a card-style format
|
||||||
|
* @param {Array} data - Array of objects to export
|
||||||
|
* @param {string} fileName - File name for the PDF (optional)
|
||||||
|
*/
|
||||||
|
export const exportToPDF1 = async (data, fileName = "data") => {
|
||||||
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||||
|
|
||||||
|
const pageWidth = 600;
|
||||||
|
const pageHeight = 800;
|
||||||
|
const margin = 30;
|
||||||
|
const cardSpacing = 20;
|
||||||
|
const cardPadding = 10;
|
||||||
|
let page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
|
let y = pageHeight - margin;
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const title = item.ContactName || "";
|
||||||
|
const subtitle = `by ${item.CreatedBy || ""} on ${item.CreatedAt || ""}`;
|
||||||
|
const body = item.Note || "";
|
||||||
|
|
||||||
|
const cardHeight = 80 + (body.length / 60) * 14; // approximate height for body text
|
||||||
|
|
||||||
|
if (y - cardHeight < margin) {
|
||||||
|
page = pdfDoc.addPage([pageWidth, pageHeight]);
|
||||||
|
y = pageHeight - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw card border
|
||||||
|
page.drawRectangle({
|
||||||
|
x: margin,
|
||||||
|
y: y - cardHeight,
|
||||||
|
width: pageWidth - 2 * margin,
|
||||||
|
height: cardHeight,
|
||||||
|
borderColor: rgb(0.7, 0.7, 0.7),
|
||||||
|
borderWidth: 1,
|
||||||
|
color: rgb(1, 1, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
page.drawText(title, {
|
||||||
|
x: margin + cardPadding,
|
||||||
|
y: y - 20,
|
||||||
|
font: boldFont,
|
||||||
|
size: 12,
|
||||||
|
color: rgb(0.1, 0.1, 0.1),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw subtitle
|
||||||
|
page.drawText(subtitle, {
|
||||||
|
x: margin + cardPadding,
|
||||||
|
y: y - 35,
|
||||||
|
font,
|
||||||
|
size: 10,
|
||||||
|
color: rgb(0.4, 0.4, 0.4),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw body text (wrap manually)
|
||||||
|
const lines = body.match(/(.|[\r\n]){1,80}/g) || [];
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
page.drawText(line, {
|
||||||
|
x: margin + cardPadding,
|
||||||
|
y: y - 50 - i * 12,
|
||||||
|
font,
|
||||||
|
size: 10,
|
||||||
|
color: rgb(0.2, 0.2, 0.2),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
y -= cardHeight + cardSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `${fileName}.pdf`;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print the HTML table by accepting the table element or a reference.
|
* Print the HTML table by accepting the table element or a reference.
|
||||||
* @param {HTMLElement} table - The table element (or ref) to print
|
* @param {HTMLElement} table - The table element (or ref) to print
|
||||||
*/
|
*/
|
||||||
export const printTable = (table) => {
|
export const printTable = (table) => {
|
||||||
if (table) {
|
if (table) {
|
||||||
const newWindow = window.open("", "", "width=600,height=600"); // Open a new window
|
const clone = table.cloneNode(true);
|
||||||
|
// Remove last column (Actions) from all rows
|
||||||
|
clone.querySelectorAll("tr").forEach((row) => {
|
||||||
|
row.removeChild(row.lastElementChild);
|
||||||
|
});
|
||||||
|
|
||||||
// Inject styles for the table and body
|
const newWindow = window.open("", "", "width=600,height=600");
|
||||||
newWindow.document.write("<html><head><title>Print Table</title>");
|
newWindow.document.write("<html><head><title>Print Table</title>");
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.innerHTML = `
|
style.innerHTML = `
|
||||||
@ -171,16 +211,14 @@ export const printTable = (table) => {
|
|||||||
th { background-color: #f2f2f2; }
|
th { background-color: #f2f2f2; }
|
||||||
`;
|
`;
|
||||||
newWindow.document.head.appendChild(style);
|
newWindow.document.head.appendChild(style);
|
||||||
|
|
||||||
newWindow.document.write("</head><body>");
|
newWindow.document.write("</head><body>");
|
||||||
newWindow.document.write(table.outerHTML); // Write the table HTML to the new window
|
newWindow.document.write(clone.outerHTML);
|
||||||
newWindow.document.write("</body></html>");
|
newWindow.document.write("</body></html>");
|
||||||
|
newWindow.document.close();
|
||||||
newWindow.document.close(); // Close the document stream
|
|
||||||
|
|
||||||
// Wait for the document to load before triggering print
|
|
||||||
newWindow.onload = () => {
|
newWindow.onload = () => {
|
||||||
newWindow.print(); // Trigger the print dialog after the content is loaded
|
newWindow.print();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user