Compare commits
No commits in common. "bd4a3656e736105ff129ded0ee0380dbd4bb03fc" and "345822885fba38eba3bca7cf99f2e568df88cb44" have entirely different histories.
bd4a3656e7
...
345822885f
|
|
@ -18,11 +18,9 @@
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"os-utils": "^0.0.14",
|
"os-utils": "^0.0.14",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"quill": "^2.0.3",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill-new": "^3.6.0",
|
"react-quill-new": "^3.6.0",
|
||||||
"react-quill-new": "^3.6.0",
|
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
|
|
@ -6052,12 +6050,6 @@
|
||||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz",
|
||||||
|
|
@ -8262,18 +8254,6 @@
|
||||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.clonedeep": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
|
@ -9554,12 +9534,6 @@
|
||||||
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/parchment": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
|
@ -10001,21 +9975,6 @@
|
||||||
"react-dom": "^16 || ^17 || ^18 || ^19"
|
"react-dom": "^16 || ^17 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-quill-new": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-weU6YfB2+7Cujw5Hjgmi0aN/qJd3B6ADWrxgUJMp2MO3tEvKX5kfB0sg3P0UdOVfU0z8icsKFzlnEIpeW1mLhw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"quill": "~2.0.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"quill-delta": "^5.1.0",
|
|
||||||
"react": "^16 || ^17 || ^18 || ^19",
|
|
||||||
"react-dom": "^16 || ^17 || ^18 || ^19"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,8 @@
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"os-utils": "^0.0.14",
|
"os-utils": "^0.0.14",
|
||||||
"quill": "^2.0.3",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill-new": "^3.6.0",
|
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
|
|
@ -126,4 +124,4 @@
|
||||||
"vite": "^5.4.7",
|
"vite": "^5.4.7",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ const httpsAgent = new https.Agent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const api: AxiosInstance = axios.create({
|
const api: AxiosInstance = axios.create({
|
||||||
baseURL: 'http://localhost:3000/api/v1/va', // 🔁 your backend
|
baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // 🔁 your backend
|
||||||
httpsAgent
|
httpsAgent
|
||||||
// timeout: 10000,
|
// timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const handleLogin = async (credentials: { email: string, password: string
|
||||||
try {
|
try {
|
||||||
console.log("➡️ Calling API...");
|
console.log("➡️ Calling API...");
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'http://localhost:3000/api/v1/va/login',
|
'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login',
|
||||||
{ userDetails: { ...credentials } },
|
{ userDetails: { ...credentials } },
|
||||||
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
|
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,6 @@ export const useActiveAppTracker = (
|
||||||
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
|
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
|
||||||
if (!details || !details.id) return;
|
if (!details || !details.id) return;
|
||||||
|
|
||||||
console.log("details" , details)
|
|
||||||
|
|
||||||
const title = details.title;
|
const title = details.title;
|
||||||
const appName = details.owner.name;
|
const appName = details.owner.name;
|
||||||
const prevTitle = lastTitleRef.current;
|
const prevTitle = lastTitleRef.current;
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,12 @@ import {
|
||||||
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "../store";
|
import { RootState } from "../store";
|
||||||
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime, updateProjectTracking } from "../store/projectSlice";
|
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime } from "../store/projectSlice";
|
||||||
import { clearUser } from "../store/userSlice";
|
import { clearUser } from "../store/userSlice";
|
||||||
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
||||||
import { canStartTracking } from "../utils/projectLimits";
|
import { canStartTracking } from "../utils/projectLimits";
|
||||||
import { useTheme } from "../helper/ThemeToggle";
|
import { useTheme } from "../helper/ThemeToggle";
|
||||||
import LogoutModal from "../modal/LogoutModal";
|
import LogoutModal from "../modal/LogoutModal";
|
||||||
import { parseTimeStringToSeconds } from "../utils/timeParser";
|
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -78,8 +77,7 @@ export interface TrackingState {
|
||||||
handleProjectSelect: (projectId: string) => void;
|
handleProjectSelect: (projectId: string) => void;
|
||||||
projectsData: Project[];
|
projectsData: Project[];
|
||||||
loadingStart: boolean,
|
loadingStart: boolean,
|
||||||
loadingStop: boolean,
|
loadingStop: boolean
|
||||||
isStopping: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardLayout = () => {
|
const DashboardLayout = () => {
|
||||||
|
|
@ -93,7 +91,6 @@ const DashboardLayout = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [isStopping, setIsStopping] = useState(false);
|
|
||||||
|
|
||||||
const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
|
const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
|
||||||
useSelector((state: any) => state.projects);
|
useSelector((state: any) => state.projects);
|
||||||
|
|
@ -126,7 +123,6 @@ const DashboardLayout = () => {
|
||||||
const [loadingStop, setLoadingStop] = useState(false);
|
const [loadingStop, setLoadingStop] = useState(false);
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
@ -144,24 +140,16 @@ const DashboardLayout = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentStopTime = new Date().toISOString();
|
|
||||||
const projectId = selectedProject.projectId;
|
const projectId = selectedProject.projectId;
|
||||||
|
|
||||||
console.log(" Stop tracking initiated for:", selectedProject.projectName);
|
console.log(" Stop tracking initiated for:", selectedProject.projectName);
|
||||||
console.log(" Current elapsed time:", elapsedTime);
|
console.log(" Current elapsed time:", elapsedTime);
|
||||||
|
|
||||||
setIsStopping(true);
|
|
||||||
setLoadingStop(true);
|
setLoadingStop(true);
|
||||||
handleSocketDisconnect();
|
handleSocketDisconnect();
|
||||||
|
|
||||||
// STEP 1: Clear timer FIRST to prevent further client-side updates
|
// STEP 1: Send final PATCH with last active window data
|
||||||
if (timerRef.current) {
|
// This ensures the last window activity is recorded before logout
|
||||||
clearInterval(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
console.log("Timer cleared");
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP 2: Send final PATCH with last active window data
|
|
||||||
if (stopTrackingRef.current) {
|
if (stopTrackingRef.current) {
|
||||||
try {
|
try {
|
||||||
console.log(" Sending final PATCH to record last activity...");
|
console.log(" Sending final PATCH to record last activity...");
|
||||||
|
|
@ -169,7 +157,19 @@ const DashboardLayout = () => {
|
||||||
console.log(" Final tracking data sent successfully");
|
console.log(" Final tracking data sent successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(" Error sending final tracking data:", error);
|
console.error(" Error sending final tracking data:", error);
|
||||||
|
// Continue with stop even if PATCH fails
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ No finalizeTracking function available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: Stop timer everywhere
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
console.log("Timer cleared");
|
||||||
|
} else {
|
||||||
|
console.log("No active timer to clear");
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 3: Send LOGOUT to backend
|
// STEP 3: Send LOGOUT to backend
|
||||||
|
|
@ -178,7 +178,6 @@ const DashboardLayout = () => {
|
||||||
vaId: projectSummary.vaId,
|
vaId: projectSummary.vaId,
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
type: "LOGOUT",
|
type: "LOGOUT",
|
||||||
time: currentStopTime
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📡 Sending LOGOUT request:", payload);
|
console.log("📡 Sending LOGOUT request:", payload);
|
||||||
|
|
@ -188,7 +187,17 @@ const DashboardLayout = () => {
|
||||||
console.error(" Failed to send LOGOUT:", error);
|
console.error(" Failed to send LOGOUT:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 4: Update Redux tracking state to stop tracking
|
// STEP 4: Update UI (window restore)
|
||||||
|
try {
|
||||||
|
await window.electron.toggleCloseBtn(true);
|
||||||
|
await window.electron.resizeWindow(500, 600);
|
||||||
|
toggleWindowSize.current = true;
|
||||||
|
console.log(" Window UI restored");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(" Error restoring window:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 5: Update Redux tracking state → THIS TRIGGERS UI UPDATE
|
||||||
console.log("🔄 Updating Redux state to stop tracking...");
|
console.log("🔄 Updating Redux state to stop tracking...");
|
||||||
dispatch(
|
dispatch(
|
||||||
stopTrackingReducer({
|
stopTrackingReducer({
|
||||||
|
|
@ -198,82 +207,70 @@ const DashboardLayout = () => {
|
||||||
|
|
||||||
// Allow React to process the state update
|
// Allow React to process the state update
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
console.log(" Redux state updated - isTracking is now false");
|
||||||
|
|
||||||
// STEP 5: Refresh project list to get updated work hours from server
|
// STEP 6: Clear active project log ID
|
||||||
|
setActiveProjectLogId("");
|
||||||
|
|
||||||
|
// ✨ NEW STEP 7: Refresh project list to get updated work hours
|
||||||
try {
|
try {
|
||||||
console.log("🔄 Refreshing project list...");
|
console.log("🔄 Refreshing project list...");
|
||||||
const projectListRes = await window.electron.getProjectList();
|
const res = await window.electron.getProjectList();
|
||||||
console.log("📊 Updated project list:", projectListRes);
|
console.log("📊 Updated project list:", res);
|
||||||
|
|
||||||
if (projectListRes?.success && Array.isArray(projectListRes.data)) {
|
if (res?.success && Array.isArray(res.data)) {
|
||||||
// Update projects data
|
// Update projects data
|
||||||
dispatch(setProjectsDataSlice(projectListRes.data));
|
dispatch(setProjectsDataSlice(res.data));
|
||||||
|
|
||||||
// Update project summary
|
// Update project summary
|
||||||
dispatch(
|
dispatch(
|
||||||
setProjectSummarySlice({
|
setProjectSummarySlice({
|
||||||
totalProjects: projectListRes.totalProjects || 0,
|
totalProjects: res.totalProjects || 0,
|
||||||
activeProjects: projectListRes.activeProjects || 0,
|
activeProjects: res.activeProjects || 0,
|
||||||
awayProjects: projectListRes.awayProjects || 0,
|
awayProjects: res.awayProjects || 0,
|
||||||
totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s",
|
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
|
||||||
totalWorkMs: projectListRes.totalWorkMs || 0,
|
totalWorkMs: res.totalWorkMs || 0,
|
||||||
vaName: projectListRes.vaName || "",
|
vaName: res.vaName || "",
|
||||||
vaId: projectListRes.vaId || "",
|
vaId: res.vaId || "",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update selected project with fresh data
|
// Update selected project with fresh data
|
||||||
const updatedProject = projectListRes.data.find(
|
const updatedProject = res.data.find(
|
||||||
(p: Project) => p.projectId === projectId
|
(p: Project) => p.projectId === projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (updatedProject) {
|
if (updatedProject) {
|
||||||
dispatch(setSelectedProjectSlice(updatedProject));
|
dispatch(setSelectedProjectSlice(updatedProject));
|
||||||
|
|
||||||
// CRITICAL FIX: Use server time instead of client elapsedTime
|
// Update tracking map with new total work time
|
||||||
const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime);
|
const initialElapsed = Math.floor((updatedProject.totalWorkMs || 0) / 1000);
|
||||||
|
|
||||||
console.log("🔄 Syncing client time with server time:", {
|
|
||||||
serverTime: updatedProject.totalWorkTime,
|
|
||||||
serverElapsedSeconds: serverElapsedTime,
|
|
||||||
previousClientTime: elapsedTime,
|
|
||||||
difference: serverElapsedTime - elapsedTime
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force update the tracking map with server time
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateProjectTracking({
|
initializeProjectTracking({
|
||||||
projectId: updatedProject.projectId,
|
projectId: updatedProject.projectId,
|
||||||
data: {
|
initialTime: initialElapsed,
|
||||||
elapsedTime: serverElapsedTime,
|
|
||||||
accumulatedTime: serverElapsedTime,
|
|
||||||
isTracking: false,
|
|
||||||
isPaused: false,
|
|
||||||
startTime: null,
|
|
||||||
endTime: Date.now(),
|
|
||||||
pauseStartTime: null,
|
|
||||||
totalPausedTime: 0
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("✅ Selected project updated with server time");
|
console.log("✅ Selected project updated with fresh data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 6: Update UI (window restore)
|
|
||||||
await window.electron.toggleCloseBtn(true);
|
|
||||||
await window.electron.resizeWindow(500, 600);
|
|
||||||
toggleWindowSize.current = true;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error refreshing project list:", error);
|
console.error("❌ Error refreshing project list:", error);
|
||||||
} finally {
|
|
||||||
// Clear loading states
|
|
||||||
setLoadingStop(false);
|
|
||||||
setIsStopping(false);
|
|
||||||
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoadingStop(false);
|
||||||
|
console.log("🧹 Active project log ID cleared");
|
||||||
|
|
||||||
|
console.log("=".repeat(50));
|
||||||
|
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
|
||||||
|
console.log(`Project: ${selectedProject.projectName}`);
|
||||||
|
console.log(`Final Time: ${formatTime(elapsedTime)}`);
|
||||||
|
console.log("UI Updates:");
|
||||||
|
console.log(" ✓ Header should be hidden");
|
||||||
|
console.log(" ✓ Start button should be visible");
|
||||||
|
console.log(" ✓ Layout should expand to full height");
|
||||||
|
console.log("=".repeat(50));
|
||||||
}, [selectedProject, projectSummary, elapsedTime, dispatch]);
|
}, [selectedProject, projectSummary, elapsedTime, dispatch]);
|
||||||
|
|
||||||
// NOW define handleLimitExceeded using the defined handleStopTracking
|
// NOW define handleLimitExceeded using the defined handleStopTracking
|
||||||
|
|
@ -364,9 +361,10 @@ const DashboardLayout = () => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const handleStartTracking = async (project: Project) => {
|
const handleStartTracking = async (project: Project) => {
|
||||||
|
|
||||||
if (loadingStart || loadingStop) return;
|
if (loadingStart || loadingStop) return;
|
||||||
|
|
||||||
// Check limit before starting
|
// Check limit before starting
|
||||||
const limitCheck = canStartTracking(project);
|
const limitCheck = canStartTracking(project);
|
||||||
if (!limitCheck.canStart) {
|
if (!limitCheck.canStart) {
|
||||||
alert(
|
alert(
|
||||||
|
|
@ -383,14 +381,13 @@ const DashboardLayout = () => {
|
||||||
setLoadingStart(true);
|
setLoadingStart(true);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let currentStartTime = new Date().toISOString();
|
|
||||||
window.electron.toggleCloseBtn(false);
|
window.electron.toggleCloseBtn(false);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
vaId: projectSummary.vaId,
|
vaId: projectSummary.vaId,
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
type: "LOGIN",
|
type: "LOGIN",
|
||||||
time: currentStartTime
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -402,15 +399,6 @@ const DashboardLayout = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Get the current accumulated time BEFORE starting timer
|
|
||||||
const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime);
|
|
||||||
|
|
||||||
console.log("📊 Starting with accumulated time:", {
|
|
||||||
accumulatedTime: currentAccumulatedTime,
|
|
||||||
projectTotalWorkTime: project.totalWorkTime,
|
|
||||||
parsedTime: parseTimeStringToSeconds(project.totalWorkTime)
|
|
||||||
});
|
|
||||||
|
|
||||||
// ⬇️ REDUX UPDATE: Start Tracking
|
// ⬇️ REDUX UPDATE: Start Tracking
|
||||||
dispatch(
|
dispatch(
|
||||||
startTracking({
|
startTracking({
|
||||||
|
|
@ -419,17 +407,22 @@ const DashboardLayout = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
handleSocketConnect();
|
handleSocketConnect()
|
||||||
|
|
||||||
setActiveProjectLogId(res?.data?.logId);
|
setActiveProjectLogId(res?.data?.logId);
|
||||||
console.log(" Tracking started. Log ID:", res?.data?.logId);
|
console.log(" Tracking started. Log ID:", res?.data?.logId);
|
||||||
|
|
||||||
// ⬇️ Calculate time limit in seconds
|
// ⬇️ Calculate time limit in seconds
|
||||||
const parseTimeToSeconds = (timeStr: string): number => {
|
const parseTimeToSeconds = (timeStr: string): number => {
|
||||||
return parseTimeStringToSeconds(timeStr);
|
const hourMatch = timeStr.match(/(\d+)h/);
|
||||||
|
const minMatch = timeStr.match(/(\d+)m/);
|
||||||
|
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
|
||||||
|
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
|
||||||
|
return (hours * 3600) + (minutes * 60);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
|
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
|
||||||
const alreadyWorkedSeconds = parseTimeStringToSeconds(project.totalWorkTime);
|
const alreadyWorkedSeconds = Math.floor((project.totalWorkMs || 0) / 1000);
|
||||||
const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds;
|
const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds;
|
||||||
|
|
||||||
console.log("⏱️ Time Limits:", {
|
console.log("⏱️ Time Limits:", {
|
||||||
|
|
@ -440,19 +433,10 @@ const DashboardLayout = () => {
|
||||||
remainingMinutes: Math.floor(remainingSeconds / 60)
|
remainingMinutes: Math.floor(remainingSeconds / 60)
|
||||||
});
|
});
|
||||||
|
|
||||||
// ⬇️ FIXED: Start timer with proper initial time
|
// ⬇️ Start global timer loop with limit check
|
||||||
// Set initial elapsed time immediately to avoid jump
|
|
||||||
dispatch(
|
|
||||||
updateElapsedTime({
|
|
||||||
projectId: project.projectId,
|
|
||||||
elapsedTime: currentAccumulatedTime,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start global timer loop with limit check
|
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
const timeSinceStart = Math.floor((Date.now() - now) / 1000);
|
const currentElapsed = Math.floor((Date.now() - now) / 1000) +
|
||||||
const currentElapsed = currentAccumulatedTime + timeSinceStart;
|
(selectedData?.accumulatedTime || 0);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateElapsedTime({
|
updateElapsedTime({
|
||||||
|
|
@ -461,8 +445,8 @@ const DashboardLayout = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if time limit reached
|
// Check if time limit reached
|
||||||
const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart;
|
const totalTimeWorked = alreadyWorkedSeconds + Math.floor((Date.now() - now) / 1000);
|
||||||
|
|
||||||
if (totalTimeWorked >= dedicatedSeconds) {
|
if (totalTimeWorked >= dedicatedSeconds) {
|
||||||
console.warn("🚨 CLIENT-SIDE: Time limit reached!");
|
console.warn("🚨 CLIENT-SIDE: Time limit reached!");
|
||||||
|
|
@ -512,11 +496,9 @@ const DashboardLayout = () => {
|
||||||
const existing = projectTrackingMap[projectId];
|
const existing = projectTrackingMap[projectId];
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// Use parsed totalWorkTime instead of totalWorkMs
|
|
||||||
const initialTime = parseTimeStringToSeconds(project.totalWorkTime);
|
|
||||||
dispatch(initializeProjectTracking({
|
dispatch(initializeProjectTracking({
|
||||||
projectId,
|
projectId,
|
||||||
initialTime: initialTime
|
initialTime: Math.floor((project.totalWorkMs || 0) / 1000)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -612,8 +594,7 @@ const DashboardLayout = () => {
|
||||||
projectsData,
|
projectsData,
|
||||||
handleProjectSelect,
|
handleProjectSelect,
|
||||||
loadingStart,
|
loadingStart,
|
||||||
loadingStop,
|
loadingStop
|
||||||
isStopping
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
|
|
@ -630,13 +611,7 @@ const DashboardLayout = () => {
|
||||||
{isTracking &&
|
{isTracking &&
|
||||||
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
|
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{isStopping ? (
|
{selectedProject ? (
|
||||||
// Skeleton for left side during stop
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="skeleton w-8 h-8 rounded-full"></div>
|
|
||||||
<div className="skeleton w-32 h-4"></div>
|
|
||||||
</div>
|
|
||||||
) : selectedProject ? (
|
|
||||||
isTracking ? (
|
isTracking ? (
|
||||||
<button
|
<button
|
||||||
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
||||||
|
|
@ -650,6 +625,7 @@ const DashboardLayout = () => {
|
||||||
<Pause size={16} />
|
<Pause size={16} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
||||||
|
|
@ -669,53 +645,40 @@ const DashboardLayout = () => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isStopping && selectedProject ? (
|
{selectedProject ? (
|
||||||
<span className="ml-2 text-sm text-base-content/70">
|
<span className="ml-2 text-sm text-base-content/70">
|
||||||
{selectedProject.projectName}
|
{selectedProject.projectName}
|
||||||
</span>
|
</span>
|
||||||
) : !isStopping ? (
|
) : (
|
||||||
<span className="ml-2 text-sm text-base-content/40 italic">
|
<span className="ml-2 text-sm text-base-content/40 italic">
|
||||||
No Project Selected
|
No Project Selected
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{isStopping ? (
|
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
|
||||||
// Skeleton for right side during stop
|
{formatTime(elapsedTime || 0)}
|
||||||
<div className="flex items-center space-x-3">
|
</div>
|
||||||
<div className="skeleton w-20 h-6 rounded"></div>
|
|
||||||
<div className="skeleton w-6 h-6 rounded"></div>
|
|
||||||
<div className="skeleton w-8 h-8 rounded"></div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
|
|
||||||
{formatTime(
|
|
||||||
isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s")
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-error">
|
<span className="text-error">
|
||||||
<ShieldBan size={18} />
|
<ShieldBan size={18} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (toggleWindowSize.current) {
|
if (toggleWindowSize.current) {
|
||||||
window.electron.resizeWindow(500, 85);
|
window.electron.resizeWindow(500, 85);
|
||||||
toggleWindowSize.current = false;
|
toggleWindowSize.current = false;
|
||||||
} else {
|
} else {
|
||||||
window.electron.resizeWindow(500, 600);
|
window.electron.resizeWindow(500, 600);
|
||||||
toggleWindowSize.current = true;
|
toggleWindowSize.current = true;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-xs btn-outline btn-primary"
|
className="btn btn-xs btn-outline btn-primary"
|
||||||
>
|
>
|
||||||
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
|
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
|
||||||
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
||||||
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
|
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
|
||||||
import { parseTimeStringToSeconds } from "../utils/timeParser";
|
|
||||||
|
|
||||||
// Updated Project interface to match JSON structure
|
// Updated Project interface to match JSON structure
|
||||||
interface Project {
|
interface Project {
|
||||||
|
|
@ -41,8 +40,7 @@ interface TrackingState {
|
||||||
handleProjectSelect: (projectId: string) => void;
|
handleProjectSelect: (projectId: string) => void;
|
||||||
projectsData: Project[];
|
projectsData: Project[];
|
||||||
loadingStart: boolean,
|
loadingStart: boolean,
|
||||||
loadingStop: boolean,
|
loadingStop: boolean
|
||||||
isStopping: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function
|
// Helper function
|
||||||
|
|
@ -88,6 +86,8 @@ const Widget: React.FC<{
|
||||||
// Dashboard Component
|
// Dashboard Component
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
|
// selectedProject,
|
||||||
|
// projectTrackingMap,
|
||||||
isTracking,
|
isTracking,
|
||||||
isPaused,
|
isPaused,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
|
|
@ -96,20 +96,31 @@ const Dashboard: React.FC = () => {
|
||||||
handlePauseResume,
|
handlePauseResume,
|
||||||
handleStopTracking,
|
handleStopTracking,
|
||||||
handleProjectSelect,
|
handleProjectSelect,
|
||||||
|
// projectsData,
|
||||||
loadingStart,
|
loadingStart,
|
||||||
loadingStop,
|
loadingStop
|
||||||
isStopping
|
|
||||||
} = useOutletContext<TrackingState>();
|
} = useOutletContext<TrackingState>();
|
||||||
|
|
||||||
|
// interface AppsListInt {
|
||||||
|
// name: string;
|
||||||
|
// pid: string;
|
||||||
|
// title: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
|
||||||
|
|
||||||
const projectsData = useSelector((state: any) => state.projects.projectsData);
|
const projectsData = useSelector((state: any) => state.projects.projectsData);
|
||||||
const selectedProject = useSelector((state: any) => state.projects.selectedProject);
|
const selectedProject = useSelector((state: any) => state.projects.selectedProject);
|
||||||
const projectSummary = useSelector((state: any) => state.projects.projectSummary);
|
const projectSummary = useSelector((state: any) => state.projects.projectSummary);
|
||||||
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
|
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
|
||||||
|
|
||||||
|
|
||||||
console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
|
console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
try {
|
try {
|
||||||
dispatch(setLoading(true));
|
dispatch(setLoading(true));
|
||||||
|
|
@ -135,6 +146,20 @@ const Dashboard: React.FC = () => {
|
||||||
console.log(" Project list response in dashboard:", res);
|
console.log(" Project list response in dashboard:", res);
|
||||||
|
|
||||||
if (res?.success && Array.isArray(res.data)) {
|
if (res?.success && Array.isArray(res.data)) {
|
||||||
|
// Store project list
|
||||||
|
// setProjectsData(res.data);
|
||||||
|
|
||||||
|
// Store project summary info
|
||||||
|
// setProjectSummary({
|
||||||
|
// totalProjects: res.totalProjects || 0,
|
||||||
|
// activeProjects: res.activeProjects || 0,
|
||||||
|
// awayProjects: res.awayProjects || 0,
|
||||||
|
// totalWorkTime: res.totalWorkTime || "0h 0m 0s",
|
||||||
|
// totalWorkMs: res.totalWorkMs || 0,
|
||||||
|
// vaName: res.vaName || "",
|
||||||
|
// vaId: res.vaId || "",
|
||||||
|
// });
|
||||||
|
|
||||||
// Store data redux store
|
// Store data redux store
|
||||||
dispatch(setProjectsDataSlice(res.data));
|
dispatch(setProjectsDataSlice(res.data));
|
||||||
|
|
||||||
|
|
@ -150,16 +175,17 @@ const Dashboard: React.FC = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-select first project
|
// Auto-select first project
|
||||||
if (res.data.length > 0 && !selectedProject) {
|
if (res.data.length > 0 && !selectedProject) {
|
||||||
const firstProject = res.data[0];
|
const firstProject = res.data[0];
|
||||||
|
|
||||||
// Select the project
|
// Select the project
|
||||||
dispatch(setSelectedProjectSlice(firstProject));
|
dispatch(setSelectedProjectSlice(firstProject));
|
||||||
|
|
||||||
const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime);
|
// Convert total work ms → seconds
|
||||||
|
const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000);
|
||||||
|
|
||||||
// Initialize tracking data for first project
|
// Initialize tracking data for first project (your slice matches this)
|
||||||
dispatch(
|
dispatch(
|
||||||
initializeProjectTracking({
|
initializeProjectTracking({
|
||||||
projectId: firstProject.projectId,
|
projectId: firstProject.projectId,
|
||||||
|
|
@ -172,6 +198,9 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ Unexpected response:", res);
|
console.warn("⚠️ Unexpected response:", res);
|
||||||
|
// if (res?.status) {
|
||||||
|
// // handleLogout()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Error fetching projects:", err);
|
console.error("❌ Error fetching projects:", err);
|
||||||
|
|
@ -185,20 +214,6 @@ const Dashboard: React.FC = () => {
|
||||||
? projectTrackingMap[selectedProject.projectId]
|
? projectTrackingMap[selectedProject.projectId]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// FIX: Get the correct time to display
|
|
||||||
const getDisplayTime = () => {
|
|
||||||
if (!selectedProject) return 0;
|
|
||||||
|
|
||||||
// If tracking is active, use real-time elapsedTime
|
|
||||||
if (selectedData?.isTracking) {
|
|
||||||
return selectedData.elapsedTime || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not tracking, use server time from selectedProject
|
|
||||||
return parseTimeStringToSeconds(selectedProject.totalWorkTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayTime = getDisplayTime();
|
|
||||||
|
|
||||||
const formatWorkTime = (time: string) => {
|
const formatWorkTime = (time: string) => {
|
||||||
if (!time) return "0h 0m";
|
if (!time) return "0h 0m";
|
||||||
|
|
@ -210,7 +225,9 @@ const Dashboard: React.FC = () => {
|
||||||
return parts.slice(0, 2).join(" ");
|
return parts.slice(0, 2).join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleProjectSelectLocal = (projectId: string) => {
|
const handleProjectSelectLocal = (projectId: string) => {
|
||||||
|
|
||||||
const project = projectsData.find((p: any) => p.projectId === projectId);
|
const project = projectsData.find((p: any) => p.projectId === projectId);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
|
|
@ -219,7 +236,8 @@ const Dashboard: React.FC = () => {
|
||||||
const existingData = projectTrackingMap[projectId];
|
const existingData = projectTrackingMap[projectId];
|
||||||
|
|
||||||
if (!existingData) {
|
if (!existingData) {
|
||||||
const initialElapsed = parseTimeStringToSeconds(project.totalWorkTime);
|
const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000);
|
||||||
|
|
||||||
dispatch(initializeProjectTracking({
|
dispatch(initializeProjectTracking({
|
||||||
projectId,
|
projectId,
|
||||||
initialTime: initialElapsed
|
initialTime: initialElapsed
|
||||||
|
|
@ -233,6 +251,7 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
|
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -243,35 +262,23 @@ const Dashboard: React.FC = () => {
|
||||||
<div className="w-full mb-4">
|
<div className="w-full mb-4">
|
||||||
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
|
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{isStopping ? (
|
{selectedProject ? (
|
||||||
// Skeleton loader for tracking section
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
|
||||||
<div className="text-center sm:text-left w-full">
|
|
||||||
<div className="skeleton w-32 h-6 mx-auto sm:mx-0 mb-2"></div>
|
|
||||||
<div className="skeleton w-48 h-4 mx-auto sm:mx-0"></div>
|
|
||||||
</div>
|
|
||||||
<div className="skeleton w-full sm:w-32 h-10 rounded-xl"></div>
|
|
||||||
</div>
|
|
||||||
) : selectedProject ? (
|
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
{/* FIX: Use displayTime instead of always using server time */}
|
|
||||||
<p className="text-lg font-extrabold text-success">
|
<p className="text-lg font-extrabold text-success">
|
||||||
{formatTime(displayTime)}
|
{formatTime(selectedData?.elapsedTime || 0)}
|
||||||
</p>
|
</p>
|
||||||
|
{/* <p className="text-xs text-base-content/70 mt-1">
|
||||||
|
{selectedData?.isTracking ? "Current Session" : "Ready"}
|
||||||
|
</p> */}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base-content/70">Project:</span>{" "}
|
<span className="text-base-content/70">Project:</span>{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{selectedProject.projectName}
|
{selectedProject.projectName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Show tracking status */}
|
|
||||||
{selectedData?.isTracking && (
|
|
||||||
<div className="text-xs text-success mt-1">
|
|
||||||
● Live Tracking
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!selectedData?.isTracking ? (
|
{!selectedData?.isTracking ? (
|
||||||
|
|
@ -301,7 +308,9 @@ const Dashboard: React.FC = () => {
|
||||||
{loadingStop ? "Stopping..." : "Stop Tracking"}
|
{loadingStop ? "Stopping..." : "Stop Tracking"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider my-1" />
|
<div className="divider my-1" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -314,31 +323,19 @@ const Dashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Projects Table */}
|
{/* Projects Table */}
|
||||||
<div className="border border-base-content/10 bg-base-100 shadow-md min-h-[58vh] flex flex-col">
|
<div className=" border border-base-content/10 bg-base-100 shadow-md min-h-[58vh] flex flex-col">
|
||||||
{/* 🔹 Sticky Header */}
|
{/* 🔹 Sticky Header */}
|
||||||
<div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
|
<div className=" sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
|
||||||
<p className="text-md font-bold">Projects</p>
|
<p className="text-md font-bold">Projects</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🔹 Table or Empty State or Skeleton */}
|
{/* 🔹 Table or Empty State */}
|
||||||
{isStopping ? (
|
{projectsData.length > 0 ? (
|
||||||
// Skeleton loader for projects table
|
|
||||||
<div className="p-4 space-y-3">
|
|
||||||
{[...Array(3)].map((_, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between">
|
|
||||||
<div className="skeleton w-8 h-4"></div>
|
|
||||||
<div className="skeleton w-32 h-4"></div>
|
|
||||||
<div className="skeleton w-16 h-4"></div>
|
|
||||||
<div className="skeleton w-20 h-8 rounded"></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : projectsData.length > 0 ? (
|
|
||||||
<div
|
<div
|
||||||
className="overflow-y-auto no-scrollbar"
|
className="overflow-y-auto no-scrollbar"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: "50vh",
|
maxHeight: "50vh", // scroll only after this height
|
||||||
minHeight: "auto",
|
minHeight: "auto", // auto adjusts if few rows
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<table className="table table-zebra w-full text-[12px]">
|
<table className="table table-zebra w-full text-[12px]">
|
||||||
|
|
@ -361,19 +358,20 @@ const Dashboard: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
|
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
|
||||||
<td className="text-xs opacity-70">{index + 1}</td>
|
<td className="text-xs opacity-70">{index + 1}</td>
|
||||||
<td className="font-medium">
|
<td className="font-medium"><div className="flex flex-col">
|
||||||
<div className="flex flex-col">
|
<span className="font-medium">{project.projectName}</span>
|
||||||
<span className="font-medium">{project.projectName}</span>
|
{limitReached && (
|
||||||
{limitReached && (
|
<span className="text-xs text-error">Daily limit reached</span>
|
||||||
<span className="text-xs text-error">Daily limit reached</span>
|
)}
|
||||||
)}
|
</div></td>
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="font-mono text-xs">
|
<td className="font-mono text-xs">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-mono text-xs">
|
<span className="font-mono text-xs">
|
||||||
{formatWorkTime(project.totalWorkTime)}
|
{formatWorkTime(project.totalWorkTime)}
|
||||||
</span>
|
</span>
|
||||||
|
{/* <span className="text-xs opacity-60">
|
||||||
|
of {project.dedicatedHours}
|
||||||
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -386,6 +384,11 @@ const Dashboard: React.FC = () => {
|
||||||
>
|
>
|
||||||
{isCurrentlySelected ? "Selected" : "Select"}
|
{isCurrentlySelected ? "Selected" : "Select"}
|
||||||
</button>
|
</button>
|
||||||
|
{/* {!limitReached && (
|
||||||
|
<span className="text-xs text-success">
|
||||||
|
{remainingTime} left
|
||||||
|
</span>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -400,6 +403,8 @@ const Dashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
console.log("Leave /sick Payload:", payload);
|
console.log("Leave Payload:", payload);
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
|
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
|
||||||
console.log("Leave Payload:" ,response)
|
console.log("Leave Payload:" ,response)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import ReactQuill from 'react-quill-new';
|
|
||||||
import { quillModules } from '../../../utils/quillConfig';
|
|
||||||
import Quill from 'quill';
|
|
||||||
import 'quill/dist/quill.snow.css';
|
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
status?: string | boolean;
|
status?: string | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectSummaryResponse {
|
interface ProjectSummaryResponse {
|
||||||
|
|
@ -27,7 +23,6 @@ interface FormErrors {
|
||||||
selectedDate?: string;
|
selectedDate?: string;
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
comment?: string;
|
|
||||||
general?: string;
|
general?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +42,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
const [endTime, setEndTime] = useState("");
|
const [endTime, setEndTime] = useState("");
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const quillRef = useRef<any>(null);
|
|
||||||
|
|
||||||
// Field validation functions
|
// Field validation functions
|
||||||
const validateProjectId = (projectId: string): string | undefined => {
|
const validateProjectId = (projectId: string): string | undefined => {
|
||||||
|
|
@ -126,11 +120,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
|
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'comment':
|
|
||||||
if (!value.trim()) {
|
|
||||||
error = 'Description is required';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormErrors(prev => ({
|
setFormErrors(prev => ({
|
||||||
|
|
@ -149,9 +138,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
errors.selectedDate = validateDate(selectedDate);
|
errors.selectedDate = validateDate(selectedDate);
|
||||||
errors.startTime = validateTime(startTime, 'Start time');
|
errors.startTime = validateTime(startTime, 'Start time');
|
||||||
errors.endTime = validateTime(endTime, 'End time');
|
errors.endTime = validateTime(endTime, 'End time');
|
||||||
if (!comment.trim()) {
|
|
||||||
errors.comment = 'Description is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors.startTime && !errors.endTime) {
|
if (!errors.startTime && !errors.endTime) {
|
||||||
errors.startTime = validateTimeRange(startTime, endTime);
|
errors.startTime = validateTimeRange(startTime, endTime);
|
||||||
|
|
@ -220,39 +206,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const editor = quillRef.current?.getEditor();
|
|
||||||
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => {
|
|
||||||
return delta;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.root.addEventListener("paste", async (e: any) => {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.type.indexOf("image") === 0) {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const base64 = await convertToBase64(file);
|
|
||||||
|
|
||||||
const range = editor.getSelection();
|
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const convertToBase64 = (file: File): Promise<string> =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProjectId && selectedDate) {
|
if (selectedProjectId && selectedDate) {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
|
|
@ -330,23 +283,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
|
|
||||||
const handleDescription = (value: string) => {
|
const handleDescription = (value: string) => {
|
||||||
setComment(value);
|
setComment(value);
|
||||||
validateField('comment', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setSelectedProjectId("");
|
|
||||||
setSelectedDate("");
|
|
||||||
setStartTime("");
|
|
||||||
setEndTime("");
|
|
||||||
setComment("");
|
|
||||||
setFormErrors({});
|
|
||||||
setTotalAdjustment("0h 0m");
|
|
||||||
|
|
||||||
// Properly reset the Quill editor
|
|
||||||
if (quillRef.current) {
|
|
||||||
const editor = quillRef.current.getEditor();
|
|
||||||
editor.setContents([]); // Clear editor contents completely
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
@ -377,7 +313,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
|
|
||||||
console.log("Submitting payload:", payload);
|
console.log("Submitting payload:", payload);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await window.electron.createTimesheet(payload);
|
const res = await window.electron.createTimesheet(payload);
|
||||||
console.log("API Response:", res);
|
console.log("API Response:", res);
|
||||||
|
|
@ -385,7 +321,14 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
console.log("Timesheet Created Successfully");
|
console.log("Timesheet Created Successfully");
|
||||||
alert(res.message);
|
alert(res.message);
|
||||||
resetForm();
|
|
||||||
|
setSelectedProjectId("");
|
||||||
|
setSelectedDate("");
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
setComment("");
|
||||||
|
setFormErrors({});
|
||||||
|
setTotalAdjustment("0h 0m");
|
||||||
} else {
|
} else {
|
||||||
setFormErrors(prev => ({
|
setFormErrors(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -435,7 +378,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
</select>
|
</select>
|
||||||
{formErrors.selectedProjectId && (
|
{formErrors.selectedProjectId && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.selectedProjectId}</span>
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -453,7 +396,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
/>
|
/>
|
||||||
{formErrors.selectedDate && (
|
{formErrors.selectedDate && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.selectedDate}</span>
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -517,7 +460,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
}
|
}
|
||||||
{formErrors.startTime && (
|
{formErrors.startTime && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.startTime}</span>
|
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -539,7 +482,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
}
|
}
|
||||||
{formErrors.endTime && (
|
{formErrors.endTime && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.endTime}</span>
|
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -559,26 +502,18 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description with ReactQuill */}
|
{/* Description */}
|
||||||
<div className="form-control flex flex-col gap-1 col-span-2 mb-12">
|
<div className="form-control flex flex-col gap-1">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-semibold text-sm">Description / Reason</span>
|
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
||||||
</label>
|
</label>
|
||||||
<ReactQuill
|
<textarea
|
||||||
ref={quillRef}
|
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
|
||||||
theme="snow"
|
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
||||||
value={comment}
|
value={comment}
|
||||||
modules={quillModules}
|
onChange={(e) => handleDescription(e.target.value)}
|
||||||
onChange={handleDescription}
|
|
||||||
className="h-40"
|
|
||||||
// placeholder="Please enter the task name or leave notes to help us better understand your request."
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{formErrors.comment && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error text-xs font-medium">{formErrors.comment}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
// PtoLeaveTab.tsx
|
// PtoLeaveTab.tsx
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { quillModules } from "../../../utils/quillConfig";
|
|
||||||
import ReactQuill from "react-quill-new";
|
|
||||||
import Quill from "quill";
|
|
||||||
import "quill/dist/quill.snow.css"; // correct CSS
|
|
||||||
|
|
||||||
interface LeaveFormData {
|
interface LeaveFormData {
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
|
|
@ -21,43 +17,9 @@ const PtoLeaveTab: React.FC<{
|
||||||
toDate: "",
|
toDate: "",
|
||||||
comment: "",
|
comment: "",
|
||||||
});
|
});
|
||||||
const quillRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
|
const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const editor = quillRef.current?.getEditor();
|
|
||||||
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => {
|
|
||||||
return delta;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.root.addEventListener("paste", async (e: any) => {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.type.indexOf("image") === 0) {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const base64 = await convertToBase64(file);
|
|
||||||
|
|
||||||
const range = editor.getSelection();
|
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const convertToBase64 = (file: File): Promise<string> =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const e: Partial<LeaveFormData> = {};
|
const e: Partial<LeaveFormData> = {};
|
||||||
|
|
||||||
|
|
@ -94,30 +56,6 @@ const PtoLeaveTab: React.FC<{
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
const emptyForm: LeaveFormData = {
|
|
||||||
fromDate: "",
|
|
||||||
toDate: "",
|
|
||||||
comment: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
setPtoLeave(emptyForm);
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
// Properly reset the Quill editor
|
|
||||||
if (quillRef.current) {
|
|
||||||
const editor = quillRef.current.getEditor();
|
|
||||||
editor.setContents([]); // Clear editor contents completely
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (field: keyof LeaveFormData, value: string) => {
|
|
||||||
setPtoLeave(prev => ({ ...prev, [field]: value }));
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const e = validate();
|
const e = validate();
|
||||||
if (Object.keys(e).length) return setErrors(e);
|
if (Object.keys(e).length) return setErrors(e);
|
||||||
|
|
@ -130,13 +68,14 @@ const PtoLeaveTab: React.FC<{
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
resetForm();
|
setPtoLeave({ fromDate: "", toDate: "", comment: "" });
|
||||||
|
setErrors({});
|
||||||
alert("PTO submitted!");
|
alert("PTO submitted!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl space-y-3 ">
|
<div className="max-w-4xl space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|
||||||
{/* From Date */}
|
{/* From Date */}
|
||||||
|
|
@ -149,7 +88,7 @@ const PtoLeaveTab: React.FC<{
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={ptoLeave.fromDate}
|
value={ptoLeave.fromDate}
|
||||||
min={new Date().toISOString().split("T")[0]}
|
min={new Date().toISOString().split("T")[0]}
|
||||||
onChange={(e) => handleChange('fromDate', e.target.value)}
|
onChange={(e) => setPtoLeave({ ...ptoLeave, fromDate: e.target.value })}
|
||||||
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
|
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -166,7 +105,7 @@ const PtoLeaveTab: React.FC<{
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={ptoLeave.toDate}
|
value={ptoLeave.toDate}
|
||||||
min={new Date().toISOString().split("T")[0]}
|
min={new Date().toISOString().split("T")[0]}
|
||||||
onChange={(e) => handleChange('toDate', e.target.value)}
|
onChange={(e) => setPtoLeave({ ...ptoLeave, toDate: e.target.value })}
|
||||||
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
|
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -174,27 +113,26 @@ const PtoLeaveTab: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comment */}
|
{/* Comment */}
|
||||||
<div className="form-control mb-10 col-span-2">
|
<div className="form-control col-span-2">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-semibold text-sm">Description</span>
|
<span className="label-text font-semibold text-sm">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<ReactQuill
|
<textarea
|
||||||
ref={quillRef}
|
disabled={isSubmitting}
|
||||||
theme="snow"
|
|
||||||
value={ptoLeave.comment}
|
value={ptoLeave.comment}
|
||||||
modules={quillModules}
|
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
|
||||||
onChange={(val) => handleChange('comment', val)}
|
onChange={(e) => setPtoLeave({ ...ptoLeave, comment: e.target.value })}
|
||||||
className="h-60"
|
className={`textarea textarea-bordered text-xs h-40 ${errors.comment ? "textarea-error" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
|
||||||
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
|
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="btn btn-primary flex-1 w-full"
|
className="btn btn-primary flex-1"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<span className="loading loading-spinner loading-sm"></span>
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
|
|
@ -206,4 +144,4 @@ const PtoLeaveTab: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PtoLeaveTab;
|
export default PtoLeaveTab;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { quillModules } from "../../../utils/quillConfig";
|
|
||||||
import ReactQuill from "react-quill-new";
|
|
||||||
import Quill from "quill";
|
|
||||||
import "quill/dist/quill.snow.css"; // correct CSS
|
|
||||||
|
|
||||||
interface LeaveFormData {
|
interface LeaveFormData {
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
|
|
@ -19,42 +15,8 @@ const SickLeaveTab: React.FC<{
|
||||||
toDate: '',
|
toDate: '',
|
||||||
comment: ''
|
comment: ''
|
||||||
});
|
});
|
||||||
const quillRef = useRef<any>(null);
|
|
||||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const editor = quillRef.current?.getEditor();
|
|
||||||
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => {
|
|
||||||
return delta;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.root.addEventListener("paste", async (e: any) => {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.type.indexOf("image") === 0) {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const base64 = await convertToBase64(file);
|
|
||||||
|
|
||||||
const range = editor.getSelection();
|
|
||||||
editor.insertEmbed(range.index, "image", base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const convertToBase64 = (file: File): Promise<string> =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||||
const errors: Partial<LeaveFormData> = {};
|
const errors: Partial<LeaveFormData> = {};
|
||||||
|
|
||||||
|
|
@ -84,23 +46,6 @@ const SickLeaveTab: React.FC<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
const emptyForm: LeaveFormData = {
|
|
||||||
fromDate: "",
|
|
||||||
toDate: "",
|
|
||||||
comment: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
setSickLeave(emptyForm);
|
|
||||||
setSickErrors({});
|
|
||||||
|
|
||||||
// Properly reset the Quill editor
|
|
||||||
if (quillRef.current) {
|
|
||||||
const editor = quillRef.current.getEditor();
|
|
||||||
editor.setContents([]); // Clear editor contents completely
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const errors = validateLeaveForm(sickLeave);
|
const errors = validateLeaveForm(sickLeave);
|
||||||
|
|
||||||
|
|
@ -117,11 +62,15 @@ const SickLeaveTab: React.FC<{
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
resetForm();
|
setSickLeave({ fromDate: "", toDate: "", comment: "" });
|
||||||
|
setSickErrors({});
|
||||||
alert("SICK submitted!");
|
alert("SICK submitted!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -169,31 +118,31 @@ const SickLeaveTab: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description/Comment Field */}
|
{/* Description/Comment Field */}
|
||||||
<div className="form-control flex flex-col gap-1 col-span-2 mb-10">
|
<div className="form-control flex flex-col gap-1 col-span-2">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-semibold text-sm">Description</span>
|
<span className="label-text font-semibold text-sm">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<ReactQuill
|
<textarea
|
||||||
ref={quillRef}
|
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${sickErrors.comment ? 'textarea-error' : ''
|
||||||
theme="snow"
|
}`}
|
||||||
|
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
|
||||||
value={sickLeave.comment}
|
value={sickLeave.comment}
|
||||||
modules={quillModules}
|
disabled={isSubmitting}
|
||||||
onChange={(val) => handleSickChange('comment', val)}
|
onChange={(e) => handleSickChange('comment', e.target.value)}
|
||||||
className="h-60"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{sickErrors.comment && (
|
{sickErrors.comment && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="btn btn-primary flex-1 w-full"
|
className="btn btn-primary flex-1"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<span className="loading loading-spinner loading-sm"></span>
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
|
||||||
<td>{tabIdx + 1}</td>
|
<td>{tabIdx + 1}</td>
|
||||||
<td className="truncate max-w-[150px]" title={tab.title}>
|
<td className="truncate max-w-[150px]" title={tab.title}>
|
||||||
{tab.title} <br/>
|
{tab.title} <br/>
|
||||||
<span className="text-slate-500">{tab.appName}</span>
|
{tab.appName}
|
||||||
</td>
|
</td>
|
||||||
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br/><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
|
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br/><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
|
||||||
<td>{tab.durationFormatted}</td>
|
<td>{tab.durationFormatted}</td>
|
||||||
|
|
|
||||||
|
|
@ -113,25 +113,24 @@ const projectsSlice = createSlice({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// In your projectsSlice.ts
|
|
||||||
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
|
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
|
||||||
const p = state.projectTrackingMap[action.payload.projectId];
|
const p = state.projectTrackingMap[action.payload.projectId];
|
||||||
if (p) {
|
if (p) {
|
||||||
// Only update tracking status, time will be updated from server
|
p.accumulatedTime = p.elapsedTime;
|
||||||
p.isTracking = false;
|
p.isTracking = false;
|
||||||
p.isPaused = false;
|
p.isPaused = false;
|
||||||
p.startTime = null;
|
p.startTime = null;
|
||||||
p.pauseStartTime = null;
|
p.pauseStartTime = null;
|
||||||
p.endTime = Date.now();
|
p.endTime = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, initializeProjectTracking,
|
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, initializeProjectTracking,
|
||||||
updateProjectTracking, startTracking, updateElapsedTime, stopTracking, stopTrackingReducer } =
|
updateProjectTracking,startTracking , updateElapsedTime , stopTracking , stopTrackingReducer} =
|
||||||
projectsSlice.actions;
|
projectsSlice.actions;
|
||||||
|
|
||||||
export default projectsSlice.reducer;
|
export default projectsSlice.reducer;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue