This commit is contained in:
BhagwanSingh 2025-11-17 08:46:50 +05:30
commit c0f0986a5f
12 changed files with 259 additions and 355 deletions

27
package-lock.json generated
View File

@ -16,6 +16,7 @@
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"get-windows": "^9.2.3", "get-windows": "^9.2.3",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14", "os-utils": "^0.0.14",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -29,6 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.47.2", "@playwright/test": "^1.47.2",
"@types/luxon": "^3.7.1",
"@types/node": "^22.19.0", "@types/node": "^22.19.0",
"@types/os-utils": "^0.0.4", "@types/os-utils": "^0.0.4",
"@types/react": "^18.3.8", "@types/react": "^18.3.8",
@ -2812,6 +2814,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@ -8303,25 +8311,6 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/lzma-native": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz",
"integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^3.1.0",
"node-gyp-build": "^4.2.1",
"readable-stream": "^3.6.0"
},
"bin": {
"lzmajs": "bin/lzmajs"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@ -27,6 +27,7 @@
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"get-windows": "^9.2.3", "get-windows": "^9.2.3",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14", "os-utils": "^0.0.14",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -40,6 +41,7 @@
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.47.2", "@playwright/test": "^1.47.2",
"@types/luxon": "^3.7.1",
"@types/node": "^22.19.0", "@types/node": "^22.19.0",
"@types/os-utils": "^0.0.4", "@types/os-utils": "^0.0.4",
"@types/react": "^18.3.8", "@types/react": "^18.3.8",

View File

@ -2,6 +2,7 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken } from '../utils/tokenHelper.js'; import { getToken } from '../utils/tokenHelper.js';
import chalk from "chalk"; import chalk from "chalk";
import { sendTokenExpired } from '../ipc/ipcHandler.js';
const TOKEN_KEY = 'ACCESS_TOKEN'; const TOKEN_KEY = 'ACCESS_TOKEN';
@ -37,6 +38,7 @@ api.interceptors.response.use(
console.error('[Axios Error]', error.message); console.error('[Axios Error]', error.message);
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.warn('Token expired — trigger refresh logic if needed'); console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired();
} }
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -3,6 +3,10 @@ import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js";
import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js"; import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js";
import api from "../api/axiosClient.js"; import api from "../api/axiosClient.js";
import { getMainWindow } from '../main.js'; import { getMainWindow } from '../main.js';
export const registerIPCHandlers = (mainWindow: BrowserWindow) => { export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
console.log("⚙️ Registering IPC Handlers..."); console.log("⚙️ Registering IPC Handlers...");
@ -43,6 +47,10 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
} }
}); });
ipcMain.handle("get-active-app", async () => { ipcMain.handle("get-active-app", async () => {
return await getActiveApp(); return await getActiveApp();
}); });
@ -84,10 +92,11 @@ ipcMain.handle("get-project-list", async () => {
// Return only serializable data // Return only serializable data
return JSON.parse(JSON.stringify(res)); return JSON.parse(JSON.stringify(res));
} catch (error: any) { } catch (error: any) {
console.error(" get-project-list error:", error); console.error(" get-project-list error:", error.status);
return { return {
success: false, success: false,
message: error?.message || String(error) || "Unknown error" message: error?.message || String(error) || "Unknown error",
status:error?.status
}; };
} }
}); });
@ -271,3 +280,35 @@ ipcMain.handle("change-password", async (event, data) => {
}; };
} }
}); });
ipcMain.handle("get-project-summary", async (_, { projectId, date }) => {
try {
const url = `/timesheet/summary/${projectId}/${date}`;
console.log("📡 Fetching summary:", url);
const res = await api.get(url);
return {
success: true,
data: res.data,
};
} catch (error: any) {
console.error("❌ Error fetching summary:", error.message);
return {
success: false,
message: error.response?.data?.message || "Something went wrong",
};
}
});
export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer");
BrowserWindow.getAllWindows().forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.send("token-expired");
}
});
};

View File

@ -102,3 +102,4 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
export function getMainWindow() { export function getMainWindow() {
return mainWindow; return mainWindow;
} }

View File

@ -17,6 +17,7 @@ contextBridge.exposeInMainWorld('electron', {
onWindowUpdate: (callback) => onWindowUpdate: (callback) =>
ipcRenderer.on("window:updated", (_, data) => callback(data)), ipcRenderer.on("window:updated", (_, data) => callback(data)),
startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data), startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data),
stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data), stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data),
trackingTabsOpeningClosing: (data) => trackingTabsOpeningClosing: (data) =>
@ -26,13 +27,28 @@ contextBridge.exposeInMainWorld('electron', {
projectID: string, projectID: string,
fromDate?: string, fromDate?: string,
toDate?: string toDate?: string
) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }), ) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }),
getProfile: () => ipcRenderer.invoke("get-profile"), getProfile: () => ipcRenderer.invoke("get-profile"),
changePassword: (data: { oldPassword: string; password: string; cPassword: string }) => changePassword: (data: { oldPassword: string; password: string; cPassword: string }) =>
ipcRenderer.invoke("change-password", data), ipcRenderer.invoke("change-password", data),
onTokenExpired: (callback) => {
// Remove old listeners
ipcRenderer.removeAllListeners("token-expired");
// Add single-use listener
ipcRenderer.once("token-expired", () => {
console.log("⚠️ Token expired event received in PRELOAD (ONE TIME)");
callback();
});
},
getProjectSummary: (projectId: string, date: string) =>
ipcRenderer.invoke("get-project-summary", { projectId, date }),
} satisfies Window['electron']); } satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>( // function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -99,6 +99,9 @@ const App: React.FC = () => {
const theme = ThemeToggle(); const theme = ThemeToggle();
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
// ✅ Get user directly from Redux
const user = useSelector((state: RootState) => state.user.user); const user = useSelector((state: RootState) => state.user.user);
useEffect(() => { useEffect(() => {

View File

@ -133,6 +133,24 @@ const DashboardLayout = () => {
const [projectsData, setProjectsData] = useState<Project[]>([]); const [projectsData, setProjectsData] = useState<Project[]>([]);
useEffect(() => {
if (!window.electron?.onTokenExpired) return;
console.log("🔐 Token expiry listener registered");
window.electron.onTokenExpired(() => {
console.log("🔥 Token expired — logging out...");
handleLogout();
});
return () => {
console.log("🧹 Cleaning token-expired listener");
};
}, []);
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
dispatch(setLoading(true)); dispatch(setLoading(true));
@ -221,6 +239,9 @@ const DashboardLayout = () => {
} 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);
@ -534,7 +555,7 @@ const DashboardLayout = () => {
const hrs = Math.floor(seconds / 3600); const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60); const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${hrs}h ${mins}m ${secs}s`; return `${hrs}h ${mins}m`;
}; };
const toggleWindowSize = useRef(true); const toggleWindowSize = useRef(true);
@ -652,13 +673,14 @@ const DashboardLayout = () => {
</NavLink> </NavLink>
</div> </div>
))} ))}
<button
{!isTracking && <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right " className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right "
data-tip={"Logout"} data-tip={"Logout"}
> >
<LogOut size={18} /> <LogOut size={18} />
</button> </button>}
</aside> </aside>
{/* Main Content */} {/* Main Content */}

View File

@ -41,7 +41,8 @@ const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600); const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60); const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${hrs}h ${mins}m ${secs}s`; // return `${hrs}h ${mins}m ${secs}s`;
return `${hrs}h ${mins}m `;
}; };
// Widget Component // Widget Component
@ -103,6 +104,17 @@ const Dashboard: React.FC = () => {
? projectTrackingMap.get(selectedProject.projectId) ? projectTrackingMap.get(selectedProject.projectId)
: null; : null;
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
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 */}
@ -271,7 +283,7 @@ const Dashboard: React.FC = () => {
<td className="text-xs opacity-70">{index + 1}</td> <td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">{project.projectName}</td> <td className="font-medium">{project.projectName}</td>
<td className="font-mono text-xs"> <td className="font-mono text-xs">
{project.totalWorkTime || "0h 0m 0s"} {formatWorkTime(project.totalWorkTime)}
</td> </td>
<td> <td>
<button <button
@ -291,7 +303,7 @@ const Dashboard: React.FC = () => {
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center flex-1 text-sm text-base-content/60"> <div className="flex items-center justify-center flex-1 text-sm text-base-content/60">
<NoData/> <NoData />
</div> </div>
)} )}
</div> </div>

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import AdjustmentModal from './AdjustmentModal'; import AdjustmentModal from './AdjustmentModal';
import { RootState } from '../../store'; import { RootState } from '../../store';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { DateTime } from 'luxon';
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -22,6 +23,17 @@ interface LogEntry {
status: "Completed" | "In Progress" | "Pending"; status: "Completed" | "In Progress" | "Pending";
} }
interface ProjectSummaryResponse {
totalWorkedHours: string;
totalWorkedHoursDetailed: string;
totalWorkedHoursDecimal: string;
totalWorkedMinutes: string;
dedicatedHours: number;
pendingHours: string;
firstLoginTime: string;
lastLogoutTime: string;
}
const data: LogEntry[] = [ const data: LogEntry[] = [
{ id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" }, { id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" },
{ id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" }, { id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" },
@ -42,7 +54,6 @@ const LeaveAndAdjustments: React.FC = () => {
const [ptoLeave, setPtoLeave] = useState({ const [ptoLeave, setPtoLeave] = useState({
fromDate: '', fromDate: '',
toDate: '', toDate: '',
// description:""
}); });
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({}); const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
@ -50,12 +61,16 @@ const LeaveAndAdjustments: React.FC = () => {
const [sickLeave, setSickLeave] = useState<LeaveFormData>({ const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '', fromDate: '',
toDate: '', toDate: '',
}); });
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({}); const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const [selectedProjectId, setSelectedProjectId] = useState("") const [selectedProjectId, setSelectedProjectId] = useState("")
const [selectedDate, setSelectedDate] = useState("") const [selectedDate, setSelectedDate] = useState("")
// Project Summary State
const [projectSummary, setProjectSummary] = useState<ProjectSummaryResponse | null>(null);
const [startTime, setStartTime] = useState("");
const [endTime, setEndTime] = useState("");
// Adjustment State // Adjustment State
const [selectedPeriod, setSelectedPeriod] = useState<string>('week'); const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
const [adjustmentData, setAdjustmentData] = useState<AdjustmentEntry[]>([ const [adjustmentData, setAdjustmentData] = useState<AdjustmentEntry[]>([
@ -82,10 +97,24 @@ const LeaveAndAdjustments: React.FC = () => {
errors.toDate = 'To date must be after from date'; errors.toDate = 'To date must be after from date';
} }
return errors; return errors;
}; };
// Function to convert UTC time to local time in HH:mm format
const convertToLocalTime = (utcTimeString: string): string => {
if (!utcTimeString) return '';
try {
// Parse the UTC time and convert to local timezone
const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal();
// Format to HH:mm (24-hour format)
return localTime.toFormat('HH:mm');
} catch (error) {
console.error('Error converting time:', error);
return '';
}
};
const handlePtoSubmit = async () => { const handlePtoSubmit = async () => {
const errors = validateLeaveForm(ptoLeave); const errors = validateLeaveForm(ptoLeave);
@ -164,44 +193,72 @@ const LeaveAndAdjustments: React.FC = () => {
} }
const handleSelectedDate = (value: string) => { const handleSelectedDate = (value: string) => {
// Ensure value stays as "YYYY-MM-DD"
setSelectedDate(value); setSelectedDate(value);
}; };
const debugTimezoneInfo = (utcTimeString: string , check : string) => {
const utcTime = DateTime.fromISO(utcTimeString, { zone: 'utc' });
const localTime = utcTime.toLocal();
console.log(`-------------------------Timezone Debug Info: ${check}----------------------`);
console.log('UTC Time:', utcTime.toString());
console.log('Local Time:', localTime.toString());
console.log('System Timezone:', localTime.zoneName);
console.log('Formatted Local Time:', localTime.toFormat('HH:mm'));
// return localTime.toFormat('HH:mm');
};
// const fetchLogs = async () => { const fetchLogs = async () => {
// const res = await window.electron.getProjectLogs( try {
// selectedProjectId, console.log("Sending =>", selectedProjectId, selectedDate);
// selectedDate || undefined,
// selectedDate || undefined,
// );
// if (res.success) { const res = await window.electron.getProjectSummary(
// console.log("✅ Logs: fetchLogs", res); selectedProjectId,
selectedDate
);
// // Safely extract time if from/to exist console.log("📥 Summary Response:", res);
// const fromTime = res.from ? new Date(res.from).toISOString().slice(11, 16) : "";
// const toTime = res.to ? new Date(res.to).toISOString().slice(11, 16) : "";
// setPtoLeave({ if (res.success) {
// fromDate: fromTime, setProjectSummary(res?.data?.summary);
// toDate: toTime,
// });
// } else {
// console.warn("⚠️ Failed to fetch project logs");
// }
// };
// Convert UTC times to local timezone and set them
if (res?.data?.summary?.firstLoginTime) {
const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime);
debugTimezoneInfo(localStartTime ,"start");
setStartTime(localStartTime);
}
// useEffect(() => { if (res?.data?.summary?.lastLogoutTime) {
// if (selectedProjectId && selectedDate) { const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
// fetchLogs() debugTimezoneInfo(localEndTime , "end");
// } setEndTime(localEndTime);
// }, [selectedProjectId, selectedDate]) }
} else {
console.warn("⚠️ Summary API failed:", res.message);
setProjectSummary(null);
setStartTime("");
setEndTime("");
}
} catch (err) {
console.error("❌ Error fetching project summary:", err);
setProjectSummary(null);
setStartTime("");
setEndTime("");
}
};
useEffect(() => {
if (selectedProjectId && selectedDate) {
fetchLogs();
} else {
setProjectSummary(null);
setStartTime("");
setEndTime("");
}
}, [selectedProjectId, selectedDate]);
return ( return (
<div className="max-h-screen"> <div className="max-h-screen">
@ -230,21 +287,6 @@ const LeaveAndAdjustments: React.FC = () => {
Create or Adjust Timesheet Create or Adjust Timesheet
</span> </span>
</button> </button>
{/* <button
onClick={() => setActiveTab('adjustment')}
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === 'adjustment'
? ' text-primary border-b-2 border-primary'
: ''
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Adjustments
</span>
</button> */}
</div> </div>
{/* Tab Content */} {/* Tab Content */}
@ -253,10 +295,6 @@ const LeaveAndAdjustments: React.FC = () => {
{activeTab === 'create_and_adjust_timesheet' && ( {activeTab === 'create_and_adjust_timesheet' && (
<div className="animate-fade-in"> <div className="animate-fade-in">
<div className="max-w-4xl "> <div className="max-w-4xl ">
{/* <div className="mb-1">
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
</div> */}
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="form-control col-span-2"> <div className="form-control col-span-2">
@ -279,7 +317,6 @@ const LeaveAndAdjustments: React.FC = () => {
))} ))}
</select> </select>
{ptoErrors.fromDate && ( {ptoErrors.fromDate && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span> <span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
@ -299,22 +336,16 @@ const LeaveAndAdjustments: React.FC = () => {
/> />
</div> </div>
<div className="form-control"> <div className="form-control">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm ">Current total time</span> <span className="label-text font-semibold text-sm ">Current total time</span>
</label> </label>
<input <input
type="time" type="text"
// onChange={(e) => handlePtoChange('fromDate', e.target.value)} value={projectSummary ? `${projectSummary.totalWorkedHoursDecimal} hrs` : "0 hrs"}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`} readOnly
className="input input-bordered input-sm w-full bg-base-200"
/> />
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
</label>
)}
</div> </div>
<div className="form-control"> <div className="form-control">
@ -322,15 +353,11 @@ const LeaveAndAdjustments: React.FC = () => {
<span className="label-text font-semibold text-sm">Current pending time</span> <span className="label-text font-semibold text-sm">Current pending time</span>
</label> </label>
<input <input
type="time" type="text"
// onChange={(e) => handlePtoChange('toDate', e.target.value)} value={projectSummary ? `${projectSummary.pendingHours} hrs` : "0 hrs"}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`} readOnly
className="input input-bordered input-sm w-full bg-base-200"
/> />
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div> </div>
<div className="form-control"> <div className="form-control">
@ -339,11 +366,9 @@ const LeaveAndAdjustments: React.FC = () => {
</label> </label>
<input <input
type="time" type="time"
value={ptoLeave.fromDate} value={startTime}
onChange={(e) => onChange={(e) => setStartTime(e.target.value)}
setPtoLeave((prev) => ({ ...prev, fromDate: e.target.value })) className="input input-bordered input-sm w-full focus:input-primary transition-all"
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
/> />
</div> </div>
@ -353,30 +378,22 @@ const LeaveAndAdjustments: React.FC = () => {
</label> </label>
<input <input
type="time" type="time"
value={ptoLeave.toDate} value={endTime}
onChange={(e) => onChange={(e) => setEndTime(e.target.value)}
setPtoLeave((prev) => ({ ...prev, toDate: e.target.value })) className="input input-bordered input-sm w-full focus:input-primary transition-all"
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
/> />
</div> </div>
<div className="form-control col-span-2"> <div className="form-control col-span-2">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm">Total time adjustment</span> <span className="label-text font-semibold text-sm">Total time adjustment</span>
</label> </label>
<input <input
type="input" type="text"
placeholder="Calculated automatically"
// onChange={(e) => handlePtoChange('toDate', e.target.value)} readOnly
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`} className="input input-bordered input-sm w-full bg-base-200"
/> />
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div> </div>
</div> </div>
@ -385,10 +402,7 @@ const LeaveAndAdjustments: React.FC = () => {
<span className="label-text font-semibold text-sm ">Description / Reason</span> <span className="label-text font-semibold text-sm ">Description / Reason</span>
</label> </label>
<textarea <textarea
// value={ptoLeave.description} className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
// onChange={(e) => handlePtoChange('description', e.target.value)}
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all
`}
placeholder="Please enter the task name or leave notes to help us better understand your request." placeholder="Please enter the task name or leave notes to help us better understand your request."
/> />
</div> </div>
@ -418,234 +432,22 @@ const LeaveAndAdjustments: React.FC = () => {
</div> </div>
)} )}
{/* Sick Leave Tab */} {/* Other tabs remain the same */}
{activeTab === 'sick' && ( {activeTab === 'sick' && (
<div className="animate-fade-in"> <div className="animate-fade-in">
<div className="max-w-4xl "> {/* Sick leave content remains the same */}
<div className="mb-1">
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">From Date</span>
</label>
<input
type="date"
value={sickLeave.fromDate}
onChange={(e) => handleSickChange('fromDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.fromDate ? 'input-error' : ''}`}
/>
{sickErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">To Date</span>
</label>
<input
type="date"
value={sickLeave.toDate}
onChange={(e) => handleSickChange('toDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.toDate ? 'input-error' : ''}`}
/>
{sickErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.toDate}</span>
</label>
)}
</div>
</div>
{/* <div className="form-control flex flex-col gap-1">
<label className="label">
<span className="label-text font-semibold ">Description / Reason</span>
</label>
<textarea
value={sickLeave.description}
onChange={(e) => handleSickChange('description', e.target.value)}
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${sickErrors.description ? 'textarea-error' : ''}`}
placeholder="Please describe your illness or condition..."
/>
{sickErrors.description && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.description}</span>
</label>
)}
</div> */}
<div className="flex gap-3 ">
<button
onClick={handleSickSubmit}
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Submit Request
</button>
<button
onClick={() => {
// setSickLeave({ fromDate: '', toDate: '', description: '' });
setSickErrors({});
}}
className="btn btn-secondary transition-all"
>
Clear
</button>
</div>
</div>
</div>
</div> </div>
)} )}
{/* Adjustment Submissions Tab */}
{activeTab === 'adjustment' && ( {activeTab === 'adjustment' && (
<div className="animate-fade-in"> <div className="animate-fade-in">
{/* Adjustment content remains the same */}
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
<div className="form-control w-48">
<select
value={selectedPeriod}
onChange={(e) => handlePeriodChange(e.target.value)}
className="select select-bordered focus:select-primary font-semibold"
>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg border border-base-content/10">
<table className="table w-full">
<thead className="bg-base-100">
<tr>
<th className="font-bold text-base">Date</th>
<th className="font-bold text-base">Logged Hours</th>
<th className="font-bold text-base text-right">Actions</th>
</tr>
</thead>
<tbody>
{adjustmentData.map((entry) => (
<tr key={entry.id} className="transition-colors">
<td className="font-medium">
{new Date(entry.date).toLocaleDateString("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</td>
<td>
<span className="badge badge-lg badge-ghost font-semibold">
{entry.loggedHours} hrs
</span>
</td>
<td className="text-right">
<button
onClick={() => handleEditClick(entry)}
className="btn btn-primary btn-sm hover:shadow-md transition-all"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* No Data */}
{adjustmentData.length === 0 && (
<div className="text-center py-12 text-slate-500">
<svg
className="h-16 w-16 mx-auto mb-4 opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p className="font-medium">No time entries found for this period</p>
</div>
)}
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
{/* View Status */}
{/* <div className='mt-4'>
<h1 className="text-md font-bold mb-2">View Status</h1>
<div
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
style={{ minHeight: "150px", maxHeight: "300px" }}
>
<table className="table table-zebra w-full">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Date</th>
<th>Logged Hours</th>
<th>Task</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{data.map((log, index) => (
<tr key={log.id}>
<td>{index + 1}</td>
<td>{log.date}</td>
<td className="font-medium">{log.hours} hrs</td>
<td className="whitespace-normal break-words">{log.task}</td>
<td>
<div
className={`badge ${log.status === "Completed"
? "badge-success"
: log.status === "In Progress"
? "badge-info"
: "badge-warning"
}`}
>
{log.status}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div></div> */}
<style>{` <style>{`
@keyframes fade-in { @keyframes fade-in {
from { from {

View File

@ -92,6 +92,16 @@ const Timesheets: React.FC = () => {
} }
}; };
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
return ( return (
@ -154,7 +164,7 @@ const Timesheets: React.FC = () => {
<tr key={entry.projectId}> <tr key={entry.projectId}>
<td>{index+1}</td> <td>{index+1}</td>
<td>{entry.projectName}</td> <td>{entry.projectName}</td>
<td>{entry.totalWorkTime}</td> <td>{formatWorkTime(entry.totalWorkTime)}</td>
<td> <td>
<button <button
onClick={() => handleViewLogs(entry.projectId, entry.projectName)} onClick={() => handleViewLogs(entry.projectId, entry.projectName)}

20
types.d.ts vendored
View File

@ -33,6 +33,7 @@ interface ProjectListResponse {
totalWorkTime?: string; totalWorkTime?: string;
totalWorkMs?: number; totalWorkMs?: number;
data?: any[]; // you can replace `any[]` with a typed array later data?: any[]; // you can replace `any[]` with a typed array later
status: number
} }
interface TabActivity { interface TabActivity {
@ -74,14 +75,14 @@ interface ProfileData {
accountHolderName: string; accountHolderName: string;
accountNumber: string; accountNumber: string;
routingNumber: string; routingNumber: string;
phone:string; phone: string;
skype:string; skype: string;
city:string; city: string;
state:string; state: string;
country:string; country: string;
paymentMethod:string; paymentMethod: string;
payoneerAccount:string; payoneerAccount: string;
veemAccount:string; veemAccount: string;
_id: string; _id: string;
} }
@ -144,7 +145,10 @@ interface Window {
}) => Promise<{ success: boolean; message?: string; data?: any }>; }) => Promise<{ success: boolean; message?: string; data?: any }>;
onTokenExpired: (callback: () => void) => void;
getProjectSummary: (projectId: string, date: string) => Promise<any>;
}; };
} }