diff --git a/src/actions/bmdashboard/projectMemberAction.js b/src/actions/bmdashboard/projectMemberAction.js
new file mode 100644
index 0000000000..e0d28ebbc7
--- /dev/null
+++ b/src/actions/bmdashboard/projectMemberAction.js
@@ -0,0 +1,31 @@
+import axios from "axios";
+
+import { ENDPOINTS } from "utils/URL";
+import GET_BM_PROJECT_MEMBERS from "constants/bmdashboard/projectMemberConstants";
+import { GET_ERRORS } from "constants/errors";
+
+export const fetchBMProjectMembers = projectId => {
+ return async dispatch => {
+ axios.get(ENDPOINTS.BM_PROJECT_MEMBERS(projectId))
+ .then(res => {
+ dispatch(setProjectMembers(res.data))
+ })
+ .catch(err => {
+ dispatch(setErrors(err))
+ })
+ }
+}
+
+export const setProjectMembers = payload => {
+ return {
+ type: GET_BM_PROJECT_MEMBERS,
+ payload
+ }
+}
+
+export const setErrors = payload => {
+ return {
+ type: GET_ERRORS,
+ payload
+ }
+}
\ No newline at end of file
diff --git a/src/actions/bmdashboard/timeLoggerActions.js b/src/actions/bmdashboard/timeLoggerActions.js
new file mode 100644
index 0000000000..aa5814c8bc
--- /dev/null
+++ b/src/actions/bmdashboard/timeLoggerActions.js
@@ -0,0 +1,98 @@
+import axios from 'axios';
+import { ENDPOINTS } from '../../utils/URL';
+import { GET_ERRORS } from '../../constants/errors';
+import {
+ START_TIME_LOG,
+ PAUSE_TIME_LOG,
+ STOP_TIME_LOG,
+ GET_CURRENT_TIME_LOG,
+} from '../../constants/bmdashboard/timeLoggerConstants';
+
+// Start Time Log Action
+export const startTimeLog = (projectId, memberId, task) => {
+ return async dispatch => {
+ try {
+ const response = await axios.post(ENDPOINTS.TIME_LOGGER_START(projectId, memberId), {
+ task,
+ });
+
+ dispatch({
+ type: START_TIME_LOG,
+ payload: { ...response.data.timeLog, memberId, projectId },
+ });
+ } catch (error) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: error.response ? error.response.data : { message: 'Error starting time log' },
+ });
+ }
+ };
+};
+
+// Pause Time Log Action
+export const pauseTimeLog = (projectId, timeLogId, memberId) => {
+ return async dispatch => {
+ try {
+ const response = await axios.post(ENDPOINTS.TIME_LOGGER_PAUSE(projectId, memberId), {
+ timeLogId,
+ });
+
+ dispatch({
+ type: PAUSE_TIME_LOG,
+ payload: { ...response.data.timeLog, memberId, projectId },
+ });
+ } catch (error) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: error.response ? error.response.data : { message: 'Error pausing time log' },
+ });
+ }
+ };
+};
+
+// Stop Time Log Action
+export const stopTimeLog = (projectId, timeLogId, memberId) => {
+ return async dispatch => {
+ try {
+ const response = await axios.post(ENDPOINTS.TIME_LOGGER_STOP(projectId, memberId), {
+ timeLogId,
+ });
+
+ dispatch({
+ type: STOP_TIME_LOG,
+ payload: { ...response.data.timeLog, memberId, projectId },
+ });
+ } catch (error) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: error.response ? error.response.data : { message: 'Error stopping time log' },
+ });
+ }
+ };
+};
+
+// Get Current Time Log Action
+export const getCurrentTimeLog = (projectId, memberId) => {
+ return async dispatch => {
+ try {
+ const response = await axios.get(ENDPOINTS.TIME_LOGGER_LOGS(projectId, memberId));
+
+ // Find the ongoing or paused time log
+ const currentTimeLog = response.data.find(
+ log => log.status === 'ongoing' || log.status === 'paused',
+ );
+
+ dispatch({
+ type: GET_CURRENT_TIME_LOG,
+ payload: { currentTimeLog, memberId, projectId } || null,
+ });
+ } catch (error) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: error.response
+ ? error.response.data
+ : { message: 'Error fetching current time log' },
+ });
+ }
+ };
+};
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.css b/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.css
new file mode 100644
index 0000000000..af4380a215
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.css
@@ -0,0 +1,213 @@
+.bm-timelogger__header h1 {
+ font-size: 1.5em;
+ text-align: left;
+ margin-top: 2em;
+}
+
+.bm-dashboard__button.btn.btn-secondary {
+ background-color: #2e5061;
+}
+
+@media (min-width: 650px) {
+ .bm-dashboard__button.btn.btn-secondary {
+ padding-left: 0;
+ padding-right: 0;
+ max-height: 38px;
+ }
+}
+
+.member-card-container {
+ width: 100%;
+ display: flex;
+}
+
+
+.member-card {
+ width: 100%;
+ align-items: center;
+}
+
+.member-card-header {
+ width: 100%;
+ align-items: center;
+}
+
+.member-card-name {
+ text-align: center;
+ color: white;
+}
+
+.stopwatch-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ width: 100%;
+}
+
+.member-stopwatch {
+ background-color: #4b5055 !important;
+ border-radius: 15px !important;
+ height: 3rem;
+ font-size: 1.5rem !important;
+ text-align: center;
+ color: white;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+}
+
+.font-color-gray {
+ color: #4b5055;
+}
+
+.member-start {
+ background-color: #01987a !important;
+ border-radius: 10px !important;
+ width: 5rem;
+}
+
+.member-pause {
+ background-color: #168dc0 !important;
+ border-radius: 10px !important;
+ width: 5rem;
+}
+
+
+.member-stop {
+ background-color: #980101 !important;
+ border-radius: 10px !important;
+ width: 5rem;
+}
+
+.member-clear {
+ background-color: #a9b7be !important;
+ border-radius: 10px !important;
+ width: 5rem;
+}
+/* project summaries */
+.projects-list {
+ width: 80%;
+ list-style-type: none;
+ max-height: 80vh;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding:0;
+}
+
+.project-summary {
+ background-color: #e8f4f9;
+ min-height: 20vh;
+ height: auto;
+ margin-bottom: 2em;
+ padding: 1em;
+ border-radius: 5px;
+}
+
+.project-summary_header {
+ font-size: clamp(0.8em, 3vw, 1.3em);
+ font-weight: bold;
+ text-align: center;
+ justify-content: center;
+ /* color: #3ea0cb; */
+}
+
+.project-summary_content {
+ display: block;
+ flex-wrap: wrap;
+ text-align: start;
+ margin: 0;
+ font-size: clamp(0.9em, 2.5vw, 1em);
+}
+
+.project-summary_item {
+ padding: 8px;
+ align-items: center;
+}
+
+.project-summary_label {
+ margin-right: 1px;
+}
+
+.display-inline {
+ display: inline-block;
+ white-space: no-wrap;
+}
+
+/* error page */
+
+.bm-error-page {
+ width: 100%;
+ max-width: 1536px;
+ margin: 1rem auto;
+ padding: 0 1rem;
+}
+
+.bm-error-page section {
+ margin-top: 2rem;
+}
+
+/* forms */
+
+.inv-form-page-container {
+ width: 100%;
+ max-width: 800px;
+ margin: 1rem auto;
+ padding: 2rem;
+ border: 1px solid #ccc;
+ border-radius: 40px;
+}
+
+.inv-form {
+ margin: 2rem auto;
+}
+
+.inv-form-info {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ margin: 1rem 0;
+}
+
+.inv-form-group {
+ margin: 1.5rem auto;
+}
+
+.inv-form input,
+.inv-form select,
+.inv-form textarea {
+ background-color: rgb(249, 249, 249);
+}
+
+.inv-form-btn-group {
+ display: flex;
+ gap: 1rem;
+ margin: 4rem auto 2rem;
+}
+
+.inv-form-btn-group button {
+ width: 50%;
+}
+
+.inv-form-required::after {
+ content: '*';
+ color: red;
+}
+
+@media screen and (max-width: 800px) {
+ .inv-form-page-container {
+ width: 95%;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .inv-form-page-container h2 {
+ font-size: 1.7rem;
+ }
+}
+
+.form-footer {
+font-size: 0.875em;
+margin-top:3px;
+}
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.jsx b/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.jsx
new file mode 100644
index 0000000000..fbfe22a216
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.jsx
@@ -0,0 +1,61 @@
+import { useState, useEffect } from 'react';
+import { Container, Row, Col } from 'reactstrap';
+import { useSelector, useDispatch } from 'react-redux';
+import BMError from '../shared/BMError';
+import { fetchBMProjectMembers } from '../../../actions/bmdashboard/projectMemberAction';
+import BMTimeLogDisplayMember from './BMTimeLogDisplayMember';
+
+// function BMTimeLogCard({ selectedProject }) {
+function BMTimeLogCard(props) {
+ // const state = useSelector();
+
+ const [isError, setIsError] = useState(false);
+ const [memberList, setMemberList] = useState([]);
+ const [isMemberFetched, setIsMemberFetched] = useState(false);
+
+ const dispatch = useDispatch();
+ const errors = useSelector(state => state.errors);
+ const projectInfo = useSelector(state => state.bmProjectMembers);
+
+ useEffect(() => {
+ dispatch(fetchBMProjectMembers(props.selectedProject));
+ }, [props.selectedProject, dispatch]);
+
+ useEffect(() => {
+ if (projectInfo && projectInfo.members) {
+ setMemberList(projectInfo.members);
+ setIsMemberFetched(true);
+ }
+ }, [projectInfo]);
+
+ // trigger an error state if there is an errors object
+ useEffect(() => {
+ if (Object.entries(errors).length > 0) {
+ setIsError(true);
+ }
+ }, [errors]);
+
+ return (
+
+ {isMemberFetched && (
+
+ {memberList.map((value, index) => (
+
+
+
+ ))}
+
+ )}
+ {isError && }
+
+ );
+}
+
+export default BMTimeLogCard;
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogDisplayMember.jsx b/src/components/BMDashboard/BMTimeLogger/BMTimeLogDisplayMember.jsx
new file mode 100644
index 0000000000..5f6cdc0565
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogDisplayMember.jsx
@@ -0,0 +1,34 @@
+import { CardHeader, Card } from 'reactstrap';
+import BMTimeLogStopWatch from './BMTimeLogStopWatch';
+import './BMTimeLogCard.css';
+
+// function BMTimeLogCard({ selectedProject }) {
+function BMTimeLogDisplayMember({ firstName, lastName, role, memberId, projectId }) {
+ const roleColors = {
+ volunteer: '#78bdda', // light blue
+ core: '#ecb16c', // light orange
+ manager: '#6cc3b2', // light green
+ mentor: '#6cc3b2', // light green
+ owner: '#c36c6c', // light red
+ };
+
+ const roleKey = role.toLowerCase();
+ const cardColor = roleColors[roleKey] || '#78bdda';
+ const borderProperty = `2px solid ${cardColor}`;
+
+ return (
+
+
+
+
+ {firstName} {lastName}
+
+
+
+
+
+
+ );
+}
+
+export default BMTimeLogDisplayMember;
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogStopWatch.jsx b/src/components/BMDashboard/BMTimeLogger/BMTimeLogStopWatch.jsx
new file mode 100644
index 0000000000..d4e99c368b
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogStopWatch.jsx
@@ -0,0 +1,145 @@
+import { Button, CardBody, Row, Col, Container } from 'reactstrap';
+import { useState, useEffect, useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import moment from 'moment';
+import './BMTimeLogCard.css';
+import {
+ startTimeLog,
+ pauseTimeLog,
+ stopTimeLog,
+ getCurrentTimeLog,
+} from '../../../actions/bmdashboard/timeLoggerActions';
+
+function BMTimeLogStopWatch({ projectId, memberId }) {
+ const dispatch = useDispatch();
+ const currentTimeLog = useSelector(
+ state => state.bmTimeLogger?.bmTimeLogs?.[`${memberId}_${projectId}`] || null,
+ );
+
+ // const initialElapsedTimeRef = useRef(0);
+ const [time, setTime] = useState(0);
+ const [currentTime, setCurrentTime] = useState('');
+ const [startButtonText, setStartButtonText] = useState('START');
+ const [isStarted, setIsStarted] = useState(false);
+
+ const formatTime = useCallback(totalSeconds => {
+ const hrs = Math.floor(totalSeconds / 3600);
+ const mins = Math.floor((totalSeconds % 3600) / 60);
+ const secs = totalSeconds % 60;
+ return { hr: hrs, min: mins, sec: secs };
+ }, []);
+
+ // Fetch current time log on component mount
+ useEffect(() => {
+ dispatch(getCurrentTimeLog(projectId, memberId));
+ }, [dispatch, projectId, memberId]);
+
+ // Sync time with backend time log
+ useEffect(() => {
+ if (currentTimeLog) {
+ if (currentTimeLog.status === 'ongoing' || currentTimeLog.status === 'paused') {
+ const elapsedTime = Math.floor((currentTimeLog.totalElapsedTime || 0) / 1000);
+ // initialElapsedTimeRef.current = elapsedTime;
+ setTime(elapsedTime);
+ setIsStarted(currentTimeLog.status === 'ongoing');
+ setStartButtonText(currentTimeLog.status === 'ongoing' ? 'PAUSE' : 'START');
+ setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
+ }
+ }
+ }, [currentTimeLog]);
+
+ // eslint-disable-next-line consistent-return
+ useEffect(() => {
+ let intervalId;
+ if (isStarted) {
+ intervalId = setInterval(() => {
+ setTime(prevTime => prevTime + 1);
+ }, 1000);
+ }
+ return () => {
+ if (intervalId) clearInterval(intervalId);
+ };
+ }, [isStarted]);
+
+ // Start/Pause Handler
+ const startStop = () => {
+ if (!currentTime) {
+ setCurrentTime(moment().format('hh:mm:ss A'));
+ }
+
+ if (isStarted) {
+ // Pause the time log
+ dispatch(pauseTimeLog(projectId, currentTimeLog._id, memberId));
+ setStartButtonText('START');
+ } else {
+ // Start or resume time log
+ if (currentTimeLog) {
+ // Resume existing time log
+ dispatch(startTimeLog(projectId, memberId, currentTimeLog.task));
+ } else {
+ // Start new time log
+ dispatch(startTimeLog(projectId, memberId, 'Default Task'));
+ }
+ setStartButtonText('PAUSE');
+ }
+ setIsStarted(!isStarted);
+ };
+
+ // Stop Handler
+ const stop = () => {
+ if (currentTimeLog) {
+ dispatch(stopTimeLog(projectId, currentTimeLog._id, memberId));
+ }
+ setTime(0);
+ setCurrentTime('');
+ setStartButtonText('START');
+ setIsStarted(false);
+ // initialElapsedTimeRef.current = 0;
+ };
+
+ // Clear Handler
+ const clear = () => {
+ stop();
+ };
+
+ const { hr, min, sec } = formatTime(time);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Start at:
+
+ {currentTime}
+
+
+ {/* Task:
*/}
+
+
+
+
+
+ );
+}
+
+export default BMTimeLogStopWatch;
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.css b/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.css
new file mode 100644
index 0000000000..dbd8e512f3
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.css
@@ -0,0 +1,143 @@
+.bm-timelogger__header h1 {
+ font-size: 1.5em;
+ text-align: left;
+ margin-top: 2em;
+}
+
+.bm-dashboard__button.btn.btn-secondary {
+ background-color: #2e5061;
+}
+
+@media (min-width: 650px) {
+ .bm-dashboard__button.btn.btn-secondary {
+ padding-left: 0;
+ padding-right: 0;
+ max-height: 38px;
+ }
+}
+
+/* project summaries */
+.projects-list {
+ width: 80%;
+ list-style-type: none;
+ max-height: 80vh;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding:0;
+}
+
+.project-summary {
+ background-color: #e8f4f9;
+ min-height: 20vh;
+ height: auto;
+ margin-bottom: 2em;
+ padding: 1em;
+ border-radius: 5px;
+}
+
+.project-summary_header {
+ font-size: clamp(0.8em, 3vw, 1.3em);
+ font-weight: bold;
+ text-align: center;
+ justify-content: center;
+ /* color: #3ea0cb; */
+}
+
+.project-summary_content {
+ display: block;
+ flex-wrap: wrap;
+ text-align: start;
+ margin: 0;
+ font-size: clamp(0.9em, 2.5vw, 1em);
+}
+
+.project-summary_item {
+ padding: 8px;
+ align-items: center;
+}
+
+.project-summary_label {
+ margin-right: 1px;
+}
+
+.display-inline {
+ display: inline-block;
+ white-space: no-wrap;
+}
+
+/* error page */
+
+.bm-error-page {
+ width: 100%;
+ max-width: 1536px;
+ margin: 1rem auto;
+ padding: 0 1rem;
+}
+
+.bm-error-page section {
+ margin-top: 2rem;
+}
+
+/* forms */
+
+.inv-form-page-container {
+ width: 100%;
+ max-width: 800px;
+ margin: 1rem auto;
+ padding: 2rem;
+ border: 1px solid #ccc;
+ border-radius: 40px;
+}
+
+.inv-form {
+ margin: 2rem auto;
+}
+
+.inv-form-info {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ margin: 1rem 0;
+}
+
+.inv-form-group {
+ margin: 1.5rem auto;
+}
+
+.inv-form input,
+.inv-form select,
+.inv-form textarea {
+ background-color: rgb(249, 249, 249);
+}
+
+.inv-form-btn-group {
+ display: flex;
+ gap: 1rem;
+ margin: 4rem auto 2rem;
+}
+
+.inv-form-btn-group button {
+ width: 50%;
+}
+
+.inv-form-required::after {
+ content: '*';
+ color: red;
+}
+
+@media screen and (max-width: 800px) {
+ .inv-form-page-container {
+ width: 95%;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .inv-form-page-container h2 {
+ font-size: 1.7rem;
+ }
+}
+
+.form-footer {
+font-size: 0.875em;
+margin-top:3px;
+}
diff --git a/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.jsx b/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.jsx
new file mode 100644
index 0000000000..199c08a648
--- /dev/null
+++ b/src/components/BMDashboard/BMTimeLogger/BMTimeLogger.jsx
@@ -0,0 +1,71 @@
+import { useState, useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+// import Select from 'react-select';
+import moment from 'moment';
+import { Row, Container, Col, Input } from 'reactstrap';
+import { fetchBMProjects } from '../../../actions/bmdashboard/projectActions';
+import BMTimeLogCard from './BMTimeLogCard';
+import BMError from '../shared/BMError';
+import './BMTimeLogger.css';
+
+function BMTimeLogger() {
+ const [isError, setIsError] = useState(false);
+ const [selectedProject, setselectedProject] = useState(null);
+
+ const dispatch = useDispatch();
+ const errors = useSelector(state => state.errors);
+ const projects = useSelector(state => state.bmProjects);
+ // fetch projects data on pageload
+ useEffect(() => {
+ dispatch(fetchBMProjects());
+ }, [dispatch]);
+
+ // trigger an error state if there is an errors object
+ useEffect(() => {
+ if (Object.entries(errors).length > 0) {
+ setIsError(true);
+ }
+ }, [errors]);
+
+ return (
+
+
+
+ Member Group Check In
+
+
+
+ {/* select project dropdown */}
+
+ Date: {moment().format('MM/DD/YY')}
+
+ Project:
+
+
+ {
+ const { value } = e.target;
+ setselectedProject(value);
+ }}
+ >
+
+ {projects.map(project => (
+
+ ))}
+
+
+
+
+ {selectedProject && }
+ {isError && }
+
+ );
+}
+
+export default BMTimeLogger;
diff --git a/src/constants/bmdashboard/projectMemberConstants.js b/src/constants/bmdashboard/projectMemberConstants.js
new file mode 100644
index 0000000000..0a9d5c78fa
--- /dev/null
+++ b/src/constants/bmdashboard/projectMemberConstants.js
@@ -0,0 +1,3 @@
+const GET_BM_PROJECT_MEMBERS = 'GET_BM_PROJECT_MEMBERS';
+export const GET_BM_PROJECT_BY_ID = 'GET_BM_PROJECT_BY_ID';
+export default GET_BM_PROJECT_MEMBERS;
diff --git a/src/constants/bmdashboard/timeLoggerConstants.js b/src/constants/bmdashboard/timeLoggerConstants.js
new file mode 100644
index 0000000000..cf6aa8d9bd
--- /dev/null
+++ b/src/constants/bmdashboard/timeLoggerConstants.js
@@ -0,0 +1,4 @@
+export const START_TIME_LOG = 'START_TIME_LOG';
+export const PAUSE_TIME_LOG = 'PAUSE_TIME_LOG';
+export const STOP_TIME_LOG = 'STOP_TIME_LOG';
+export const GET_CURRENT_TIME_LOG = 'GET_CURRENT_TIME_LOG';
diff --git a/src/reducers/bmdashboard/projectMemberReducer.js b/src/reducers/bmdashboard/projectMemberReducer.js
new file mode 100644
index 0000000000..020896b3f5
--- /dev/null
+++ b/src/reducers/bmdashboard/projectMemberReducer.js
@@ -0,0 +1,11 @@
+import GET_BM_PROJECT_MEMBERS from 'constants/bmdashboard/projectMemberConstants';
+
+// eslint-disable-next-line default-param-last
+export const bmProjectMemberReducer = (materials = [], action) => {
+ if (action.type === GET_BM_PROJECT_MEMBERS) {
+ return action.payload;
+ }
+ return materials;
+};
+
+export default bmProjectMemberReducer;
diff --git a/src/reducers/bmdashboard/timeLoggerReducer.js b/src/reducers/bmdashboard/timeLoggerReducer.js
new file mode 100644
index 0000000000..b3ac9980dd
--- /dev/null
+++ b/src/reducers/bmdashboard/timeLoggerReducer.js
@@ -0,0 +1,64 @@
+/* eslint-disable no-case-declarations */
+import {
+ START_TIME_LOG,
+ PAUSE_TIME_LOG,
+ STOP_TIME_LOG,
+ GET_CURRENT_TIME_LOG,
+} from '../../constants/bmdashboard/timeLoggerConstants';
+
+const initialState = {
+ bmTimeLogs: {}, // A map of { memberId_projectId: timeLog }
+ bmTimeLogHistory: [],
+};
+
+// eslint-disable-next-line default-param-last
+export const bmTimeLoggerReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case START_TIME_LOG:
+ const startKey = `${action.payload.memberId}_${action.payload.projectId}`;
+ return {
+ ...state,
+ bmTimeLogs: {
+ ...state.bmTimeLogs,
+ [startKey]: action.payload, // Store time log specific to member and project
+ },
+ bmTimeLogHistory: [...state.bmTimeLogHistory, action.payload],
+ };
+
+ case PAUSE_TIME_LOG:
+ const pauseKey = `${action.payload.memberId}_${action.payload.projectId}`;
+ return {
+ ...state,
+ bmTimeLogs: {
+ ...state.bmTimeLogs,
+ [pauseKey]: action.payload, // Update specific member's time log
+ },
+ };
+
+ case STOP_TIME_LOG:
+ const stopKey = `${action.payload.memberId}_${action.payload.projectId}`;
+ return {
+ ...state,
+ bmTimeLogs: {
+ ...state.bmTimeLogs,
+ [stopKey]: null, // Reset specific member's time log
+ },
+ bmTimeLogHistory: [...state.bmTimeLogHistory, action.payload],
+ };
+
+ case GET_CURRENT_TIME_LOG:
+ const getKey = `${action.payload.memberId}_${action.payload.projectId}`;
+ return {
+ ...state,
+ bmTimeLogs: {
+ ...state.bmTimeLogs,
+ [getKey]: action.payload, // Get specific member's time log
+ },
+ };
+
+ default:
+ return state;
+ }
+};
+
+export default bmTimeLoggerReducer;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 0b2d09c5da..2efc7e6e1c 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -51,6 +51,9 @@ import { bmInvUnitReducer } from './bmdashboard/inventoryUnitReducer';
import { consumablesReducer } from './bmdashboard/consumablesReducer';
import { toolReducer } from './bmdashboard/toolReducer';
import { equipmentReducer } from './bmdashboard/equipmentReducer';
+import { bmProjectMemberReducer } from './bmdashboard/projectMemberReducer';
+import { bmTimeLoggerReducer } from './bmdashboard/timeLoggerReducer';
+
import dashboardReducer from './dashboardReducer';
import { timeOffRequestsReducer } from './timeOffRequestReducer';
import { totalOrgSummaryReducer } from './totalOrgSummaryReducer';
@@ -104,6 +107,8 @@ const localReducers = {
bmConsumables: consumablesReducer,
bmReusables: reusablesReducer,
dashboard: dashboardReducer,
+ bmProjectMembers: bmProjectMemberReducer,
+ bmTimeLogger: bmTimeLoggerReducer,
};
const sessionReducers = {
diff --git a/src/routes.js b/src/routes.js
index c81deee3c6..e427d41aed 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -55,6 +55,7 @@ import CheckTypes from './components/BMDashboard/shared/CheckTypes';
import Toolslist from './components/BMDashboard/Tools/ToolsList';
import AddTool from './components/BMDashboard/Tools/AddTool';
import AddTeamMember from './components/BMDashboard/AddTeamMember/AddTeamMember';
+import BMTimeLogger from './components/BMDashboard/BMTimeLogger/BMTimeLogger';
// Community Portal
import CPProtectedRoute from './components/common/CPDashboard/CPProtectedRoute';
@@ -86,6 +87,7 @@ const PurchaseReusables = lazy(() =>
// const PurchaseEquipment = lazy(() =>
// import('./components/BMDashboard/PurchaseRequests/EquipmentPurchaseRequest'),
// );
+const BMTimeLogCard = lazy(() => import('./components/BMDashboard/BMTimeLogger/BMTimeLogCard'));
const ProjectDetails = lazy(() =>
import('./components/BMDashboard/Projects/ProjectDetails/ProjectDetails'),
);
@@ -415,6 +417,13 @@ export default (
exact
component={WeeklyProjectSummary}
/>
+
+
+
{/* Community Portal Routes */}
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 3d87963e91..b2600441d6 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -199,7 +199,7 @@ export const ENDPOINTS = {
BM_EQUIPMENT_TYPES: `${APIEndpoint}/bm/invtypes/equipments`,
BM_EQUIPMENT_PURCHASE: `${APIEndpoint}/bm/equipment/purchase`,
BM_PROJECTS: `${APIEndpoint}/bm/projects`,
- BM_PROJECT_BY_ID: projectId => `${APIEndpoint}/project/${projectId}`,
+ BM_PROJECT_BY_ID: projectId => `${APIEndpoint}/bm/project/${projectId}`,
BM_UPDATE_MATERIAL: `${APIEndpoint}/bm/updateMaterialRecord`,
BM_UPDATE_MATERIAL_BULK: `${APIEndpoint}/bm/updateMaterialRecordBulk`,
BM_UPDATE_MATERIAL_STATUS: `${APIEndpoint}/bm/updateMaterialStatus`,
@@ -225,6 +225,18 @@ export const ENDPOINTS = {
BM_TAG_ADD: `${APIEndpoint}/bm/tags`,
BM_TAGS_DELETE: `${APIEndpoint}/bm/tags`,
+ BM_PROJECT_MEMBERS: projectId => `${APIEndpoint}/bm/project/${projectId}/users`,
+
+ // bm time logger endpoints
+ TIME_LOGGER_START: (projectId, memberId) =>
+ `${APIEndpoint}/bm/timelogger/${projectId}/${memberId}/start`,
+ TIME_LOGGER_PAUSE: (projectId, memberId) =>
+ `${APIEndpoint}/bm/timelogger/${projectId}/${memberId}/pause`,
+ TIME_LOGGER_STOP: (projectId, memberId) =>
+ `${APIEndpoint}/bm/timelogger/${projectId}/${memberId}/stop`,
+ TIME_LOGGER_LOGS: (projectId, memberId) =>
+ `${APIEndpoint}/bm/timelogger/${projectId}/${memberId}/logs`,
+
GET_TIME_OFF_REQUESTS: () => `${APIEndpoint}/getTimeOffRequests`,
ADD_TIME_OFF_REQUEST: () => `${APIEndpoint}/setTimeOffRequest`,
UPDATE_TIME_OFF_REQUEST: id => `${APIEndpoint}/updateTimeOffRequest/${id}`,