Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/actions/bmdashboard/timeLoggerActions.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import axios from 'axios';
import moment from 'moment';
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,
GET_ALL_PROJECT_TIME_LOGS,
} from '../../constants/bmdashboard/timeLoggerConstants';

// Start Time Log Action
Expand Down Expand Up @@ -62,6 +64,12 @@
type: STOP_TIME_LOG,
payload: { ...response.data.timeLog, memberId, projectId },
});

// Refresh all project time logs after stopping to update the summary
// Small delay to ensure the completed log is saved and queryable
setTimeout(() => {
dispatch(getAllProjectTimeLogs(projectId));
}, 300);
} catch (error) {
dispatch({
type: GET_ERRORS,
Expand All @@ -77,14 +85,17 @@
try {
const response = await axios.get(ENDPOINTS.TIME_LOGGER_LOGS(projectId, memberId));

// Find the ongoing or paused time log
// Find the ongoing or paused time log, but only if it's from today
const today = moment().startOf('day');
const currentTimeLog = response.data.find(
log => log.status === 'ongoing' || log.status === 'paused',
log =>
(log.status === 'ongoing' || log.status === 'paused') &&
moment(log.createdAt).isSameOrAfter(today),
);

dispatch({
type: GET_CURRENT_TIME_LOG,
payload: { currentTimeLog, memberId, projectId } || null,
payload: { currentTimeLog: currentTimeLog || null, memberId, projectId },
});
} catch (error) {
dispatch({
Expand All @@ -96,3 +107,26 @@
}
};
};

// Get All Project Time Logs Action
export const getAllProjectTimeLogs = projectId => {
return async dispatch => {
try {
const response = await axios.get(ENDPOINTS.TIME_LOGGER_ALL_LOGS(projectId));


dispatch({
type: GET_ALL_PROJECT_TIME_LOGS,
payload: { timeLogs: response.data, projectId },
});
} catch (error) {
console.error('Error fetching project time logs:', error);

Check warning on line 123 in src/actions/bmdashboard/timeLoggerActions.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
dispatch({
type: GET_ERRORS,
payload: error.response
? error.response.data
: { message: 'Error fetching project time logs' },
});
}
};
};
23 changes: 20 additions & 3 deletions src/components/BMDashboard/BMTimeLogger/BMTimeLogCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSelector, useDispatch } from 'react-redux';
import BMError from '../shared/BMError';
import { fetchBMProjectMembers } from '../../../actions/bmdashboard/projectMemberAction';
import BMTimeLogDisplayMember from './BMTimeLogDisplayMember';
import BMTimeLogSummary from './BMTimeLogSummary';

function BMTimeLogCard(props) {
const [isError, setIsError] = useState(false);
Expand All @@ -21,9 +22,22 @@ function BMTimeLogCard(props) {
}, [props.selectedProject, dispatch]);

useEffect(() => {
if (projectInfo && projectInfo.members) {
setMemberList(projectInfo.members);
setFilteredMembers(projectInfo.members);
// Backend returns entire project object with members array
// Reducer stores it as { members: entireProjectObject }
// So projectInfo.members is the entire project, and projectInfo.members.members is the actual members array
let members = [];
if (projectInfo?.members) {
// Check if projectInfo.members is the project object (has members property) or the array itself
if (Array.isArray(projectInfo.members)) {
members = projectInfo.members;
} else if (projectInfo.members.members && Array.isArray(projectInfo.members.members)) {
members = projectInfo.members.members;
}
}

if (members.length >= 0) {
setMemberList(members);
setFilteredMembers(members);
setIsMemberFetched(true);
}
}, [projectInfo]);
Expand Down Expand Up @@ -80,6 +94,9 @@ function BMTimeLogCard(props) {

return (
<Container fluid>
{/* Time Log Summary Section */}
<BMTimeLogSummary projectId={props.selectedProject} />

{isMemberFetched && (
<>
<Row className="my-3">
Expand Down
154 changes: 142 additions & 12 deletions src/components/BMDashboard/BMTimeLogger/BMTimeLogStopWatch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
const [startButtonText, setStartButtonText] = useState('START');
const [isStarted, setIsStarted] = useState(false);
const intervalRef = useRef(null);
const isStartingNewLogRef = useRef(false);
const justPausedTimeRef = useRef(null);
const resumingFromTimeRef = useRef(null);
const justStoppedTimeRef = useRef(null);

const formatTime = useCallback(totalSeconds => {
const hrs = Math.floor(totalSeconds / 3600);
Expand All @@ -37,6 +41,73 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
// Sync time with backend time log
useEffect(() => {
if (currentTimeLog) {
// If we just started a new log, ignore old elapsed time and keep it at 0
if (isStartingNewLogRef.current && currentTimeLog.status === 'ongoing') {
isStartingNewLogRef.current = false;
setTime(0);
initialElapsedTimeRef.current = 0;
setIsStarted(true);
setStartButtonText('PAUSE');
if (currentTimeLog.createdAt) {
setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
}
return;
}

// If we just resumed from a paused state, use the time we were at, not backend's old elapsed time
if (resumingFromTimeRef.current !== null && currentTimeLog.status === 'ongoing') {
const resumeTime = resumingFromTimeRef.current;
resumingFromTimeRef.current = null;
setTime(resumeTime);
initialElapsedTimeRef.current = resumeTime;
setIsStarted(true);
setStartButtonText('PAUSE');
if (currentTimeLog.createdAt) {
setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
}
return;
}

// If we just paused, use the local time we stored, not the backend's elapsed time
if (justPausedTimeRef.current !== null && currentTimeLog.status === 'paused') {
const pausedTime = justPausedTimeRef.current;
justPausedTimeRef.current = null;
setTime(pausedTime);
initialElapsedTimeRef.current = pausedTime;
setIsStarted(false);
setStartButtonText('START');
if (currentTimeLog.createdAt) {
setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
}
return;
}

// If we just stopped, use the local time we stored, not the backend's elapsed time
if (justStoppedTimeRef.current !== null && currentTimeLog.status === 'completed') {
const stoppedTime = justStoppedTimeRef.current;
// Don't clear the ref yet - keep it until we're sure the sync is done
setTime(stoppedTime);
initialElapsedTimeRef.current = stoppedTime;
setIsStarted(false);
setStartButtonText('START');
if (currentTimeLog.createdAt) {
setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
}
// Clear the ref after a short delay to prevent re-syncing
setTimeout(() => {
justStoppedTimeRef.current = null;
}, 100);
return;
}

// If there's a stopped time ref but log is not completed yet, preserve the time
if (justStoppedTimeRef.current !== null) {
const stoppedTime = justStoppedTimeRef.current;
setTime(stoppedTime);
initialElapsedTimeRef.current = stoppedTime;
return;
}

const elapsedTime = Math.floor((currentTimeLog.totalElapsedTime || 0) / 1000);
initialElapsedTimeRef.current = elapsedTime;

Expand All @@ -46,8 +117,17 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
(Date.now() - new Date(currentTimeLog.currentIntervalStarted).getTime()) / 1000,
);
setTime(elapsedTime + additionalTime);
} else {
} else if (currentTimeLog.status === 'paused') {
// For paused logs, show the elapsed time (only if we didn't just pause)
setTime(elapsedTime);
} else if (currentTimeLog.status === 'completed') {
// For completed logs, show the final elapsed time (don't reset to 0)
setTime(elapsedTime);
initialElapsedTimeRef.current = elapsedTime;
} else {
// For other statuses, start from 0
setTime(0);
initialElapsedTimeRef.current = 0;
}

setIsStarted(currentTimeLog.status === 'ongoing');
Expand All @@ -56,6 +136,28 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
if (currentTimeLog.createdAt) {
setCurrentTime(moment(currentTimeLog.createdAt).format('hh:mm:ss A'));
}
} else {
// No current log
// If we just stopped, preserve the final time instead of resetting
if (justStoppedTimeRef.current !== null) {
const stoppedTime = justStoppedTimeRef.current;
setTime(stoppedTime);
initialElapsedTimeRef.current = stoppedTime;
setIsStarted(false);
setStartButtonText('START');
// Keep the ref so the time stays visible
// Only clear it when starting a new log
return;
}

// Otherwise, reset to 0
setTime(0);
initialElapsedTimeRef.current = 0;
setIsStarted(false);
setStartButtonText('START');
isStartingNewLogRef.current = false;
justPausedTimeRef.current = null;
resumingFromTimeRef.current = null;
}
}, [currentTimeLog]);

Expand Down Expand Up @@ -91,23 +193,33 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
if (isStarted) {
// Pause the time log - store the current time value before pausing
const currentElapsedTime = time;
justPausedTimeRef.current = currentElapsedTime;
dispatch(pauseTimeLog(projectId, currentTimeLog._id, memberId))
.then(() => {
// If pauseTimeLog fails, at least keep the UI consistent
setStartButtonText('START');
setIsStarted(false);
// UI will be updated by the sync useEffect when the paused log comes back
})
.catch(() => {
// On error, ensure UI still shows the correct time
justPausedTimeRef.current = null;
setTime(currentElapsedTime);
setStartButtonText('START');
setIsStarted(false);
});
} else {
// Start or resume time log
if (currentTimeLog && currentTimeLog.status === 'paused') {
// Resume existing time log
// Resume existing paused time log - preserve the current time
isStartingNewLogRef.current = false;
resumingFromTimeRef.current = time; // Store current time before resuming
dispatch(startTimeLog(projectId, memberId, currentTimeLog.task));
} else {
// Start new time log
// Start new time log - always reset to 0 for new logs
isStartingNewLogRef.current = true;
resumingFromTimeRef.current = null;
justStoppedTimeRef.current = null; // Clear stopped time ref when starting new
setTime(0);
initialElapsedTimeRef.current = 0;
setCurrentTime(moment().format('hh:mm:ss A'));
dispatch(startTimeLog(projectId, memberId, 'Default Task'));
}
setStartButtonText('PAUSE');
Expand All @@ -118,13 +230,31 @@ function BMTimeLogStopWatch({ projectId, memberId }) {
// Stop Handler
const stop = () => {
if (currentTimeLog) {
dispatch(stopTimeLog(projectId, currentTimeLog._id, memberId));
// Store the current time before stopping so it remains visible
const finalTime = time;
justStoppedTimeRef.current = finalTime;

// Immediately update UI to show stopped state with final time
setTime(finalTime);
initialElapsedTimeRef.current = finalTime;
setStartButtonText('START');
setIsStarted(false);
// Don't clear currentTime - keep the start time visible

// Then dispatch the stop action
dispatch(stopTimeLog(projectId, currentTimeLog._id, memberId)).catch(() => {
// On error, still show the time
justStoppedTimeRef.current = null;
setTime(finalTime);
});
} else {
// No active log, just reset
setTime(0);
setCurrentTime('');
setStartButtonText('START');
setIsStarted(false);
initialElapsedTimeRef.current = 0;
}
setTime(0);
setCurrentTime('');
setStartButtonText('START');
setIsStarted(false);
initialElapsedTimeRef.current = 0;
};

// Clear Handler
Expand Down
Loading
Loading