Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker
This commit is contained in:
commit
c0f0986a5f
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,4 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
|
||||||
export function getMainWindow() {
|
export function getMainWindow() {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
@ -33,6 +34,21 @@ 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>(
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue