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",
|
||||
"get-windows": "^9.2.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
"luxon": "^3.7.2",
|
||||
"os-utils": "^0.0.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@types/node": "^22.19.0",
|
||||
"@types/os-utils": "^0.0.4",
|
||||
"@types/react": "^18.3.8",
|
||||
|
|
@ -2812,6 +2814,12 @@
|
|||
"@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": {
|
||||
"version": "2.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"electron-store": "^11.0.2",
|
||||
"get-windows": "^9.2.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
"luxon": "^3.7.2",
|
||||
"os-utils": "^0.0.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@types/node": "^22.19.0",
|
||||
"@types/os-utils": "^0.0.4",
|
||||
"@types/react": "^18.3.8",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { getToken } from '../utils/tokenHelper.js';
|
||||
import chalk from "chalk";
|
||||
import { sendTokenExpired } from '../ipc/ipcHandler.js';
|
||||
|
||||
const TOKEN_KEY = 'ACCESS_TOKEN';
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ api.interceptors.response.use(
|
|||
console.error('[Axios Error]', error.message);
|
||||
if (error.response?.status === 401) {
|
||||
console.warn('Token expired — trigger refresh logic if needed');
|
||||
sendTokenExpired();
|
||||
}
|
||||
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 api from "../api/axiosClient.js";
|
||||
import { getMainWindow } from '../main.js';
|
||||
|
||||
|
||||
|
||||
|
||||
export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
|
||||
console.log("⚙️ Registering IPC Handlers...");
|
||||
|
||||
|
|
@ -42,6 +46,10 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
|
|||
return { success: false, message: "Logout failed" };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ipcMain.handle("get-active-app", async () => {
|
||||
return await getActiveApp();
|
||||
|
|
@ -84,10 +92,11 @@ ipcMain.handle("get-project-list", async () => {
|
|||
// Return only serializable data
|
||||
return JSON.parse(JSON.stringify(res));
|
||||
} catch (error: any) {
|
||||
console.error(" get-project-list error:", error);
|
||||
console.error(" get-project-list error:", error.status);
|
||||
return {
|
||||
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");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,4 +101,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
|
|||
|
||||
export function getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,22 +17,38 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
onWindowUpdate: (callback) =>
|
||||
ipcRenderer.on("window:updated", (_, data) => callback(data)),
|
||||
|
||||
|
||||
startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data),
|
||||
stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data),
|
||||
trackingTabsOpeningClosing: (data) =>
|
||||
ipcRenderer.invoke("tracking-tabs-opening-and-closing", data),
|
||||
|
||||
getProjectLogs: (
|
||||
projectID: string,
|
||||
fromDate?: string,
|
||||
toDate?: string
|
||||
) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }),
|
||||
projectID: string,
|
||||
fromDate?: string,
|
||||
toDate?: string
|
||||
) => 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),
|
||||
|
||||
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']);
|
||||
|
||||
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ const App: React.FC = () => {
|
|||
const theme = ThemeToggle();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
|
||||
|
||||
// ✅ Get user directly from Redux
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,24 @@ const DashboardLayout = () => {
|
|||
|
||||
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 () => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
|
|
@ -217,10 +235,13 @@ const DashboardLayout = () => {
|
|||
// Set the initial elapsed time in the component state
|
||||
setElapsedTime(initialElapsedTime);
|
||||
}
|
||||
fetchProfile()
|
||||
fetchProfile()
|
||||
|
||||
} else {
|
||||
console.warn("⚠️ Unexpected response:", res);
|
||||
// if (res?.status) {
|
||||
// // handleLogout()
|
||||
// }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching projects:", err);
|
||||
|
|
@ -534,7 +555,7 @@ const DashboardLayout = () => {
|
|||
const hrs = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${hrs}h ${mins}m ${secs}s`;
|
||||
return `${hrs}h ${mins}m`;
|
||||
};
|
||||
|
||||
const toggleWindowSize = useRef(true);
|
||||
|
|
@ -652,13 +673,14 @@ const DashboardLayout = () => {
|
|||
</NavLink>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
|
||||
{!isTracking && <button
|
||||
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 "
|
||||
data-tip={"Logout"}
|
||||
>
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
</button>}
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ const formatTime = (seconds: number) => {
|
|||
const hrs = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 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
|
||||
|
|
@ -103,6 +104,17 @@ const Dashboard: React.FC = () => {
|
|||
? projectTrackingMap.get(selectedProject.projectId)
|
||||
: 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 (
|
||||
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
|
||||
{/* Header */}
|
||||
|
|
@ -271,7 +283,7 @@ const Dashboard: React.FC = () => {
|
|||
<td className="text-xs opacity-70">{index + 1}</td>
|
||||
<td className="font-medium">{project.projectName}</td>
|
||||
<td className="font-mono text-xs">
|
||||
{project.totalWorkTime || "0h 0m 0s"}
|
||||
{formatWorkTime(project.totalWorkTime)}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
|
@ -291,7 +303,7 @@ const Dashboard: React.FC = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center flex-1 text-sm text-base-content/60">
|
||||
<NoData/>
|
||||
<NoData />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import AdjustmentModal from './AdjustmentModal';
|
||||
import { RootState } from '../../store';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
|
|
@ -22,6 +23,17 @@ interface LogEntry {
|
|||
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[] = [
|
||||
{ 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" },
|
||||
|
|
@ -42,7 +54,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
const [ptoLeave, setPtoLeave] = useState({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
// description:""
|
||||
});
|
||||
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
|
|
@ -50,12 +61,16 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
|
||||
});
|
||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||
const [selectedProjectId, setSelectedProjectId] = 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
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
|
||||
const [adjustmentData, setAdjustmentData] = useState<AdjustmentEntry[]>([
|
||||
|
|
@ -82,10 +97,24 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
errors.toDate = 'To date must be after from date';
|
||||
}
|
||||
|
||||
|
||||
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 errors = validateLeaveForm(ptoLeave);
|
||||
|
|
@ -164,44 +193,72 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
}
|
||||
|
||||
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 res = await window.electron.getProjectLogs(
|
||||
// selectedProjectId,
|
||||
// selectedDate || undefined,
|
||||
// selectedDate || undefined,
|
||||
// );
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
console.log("Sending =>", selectedProjectId, selectedDate);
|
||||
|
||||
// if (res.success) {
|
||||
// console.log("✅ Logs: fetchLogs", res);
|
||||
const res = await window.electron.getProjectSummary(
|
||||
selectedProjectId,
|
||||
selectedDate
|
||||
);
|
||||
|
||||
// // Safely extract time if from/to exist
|
||||
// const fromTime = res.from ? new Date(res.from).toISOString().slice(11, 16) : "";
|
||||
// const toTime = res.to ? new Date(res.to).toISOString().slice(11, 16) : "";
|
||||
console.log("📥 Summary Response:", res);
|
||||
|
||||
// setPtoLeave({
|
||||
// fromDate: fromTime,
|
||||
// toDate: toTime,
|
||||
// });
|
||||
// } else {
|
||||
// console.warn("⚠️ Failed to fetch project logs");
|
||||
// }
|
||||
// };
|
||||
if (res.success) {
|
||||
setProjectSummary(res?.data?.summary);
|
||||
|
||||
// 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 (selectedProjectId && selectedDate) {
|
||||
// fetchLogs()
|
||||
// }
|
||||
// }, [selectedProjectId, selectedDate])
|
||||
if (res?.data?.summary?.lastLogoutTime) {
|
||||
const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
|
||||
debugTimezoneInfo(localEndTime , "end");
|
||||
setEndTime(localEndTime);
|
||||
}
|
||||
} 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 (
|
||||
<div className="max-h-screen">
|
||||
|
|
@ -230,21 +287,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
Create or Adjust Timesheet
|
||||
</span>
|
||||
</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>
|
||||
|
||||
{/* Tab Content */}
|
||||
|
|
@ -253,10 +295,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
{activeTab === 'create_and_adjust_timesheet' && (
|
||||
<div className="animate-fade-in">
|
||||
<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="grid grid-cols-2 gap-4">
|
||||
<div className="form-control col-span-2">
|
||||
|
|
@ -279,7 +317,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
))}
|
||||
</select>
|
||||
|
||||
|
||||
{ptoErrors.fromDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
|
||||
|
|
@ -299,22 +336,16 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm ">Current total time</span>
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
// onChange={(e) => handlePtoChange('fromDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
|
||||
type="text"
|
||||
value={projectSummary ? `${projectSummary.totalWorkedHoursDecimal} hrs` : "0 hrs"}
|
||||
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 className="form-control">
|
||||
|
|
@ -322,15 +353,11 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
<span className="label-text font-semibold text-sm">Current pending time</span>
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
|
||||
type="text"
|
||||
value={projectSummary ? `${projectSummary.pendingHours} hrs` : "0 hrs"}
|
||||
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 className="form-control">
|
||||
|
|
@ -339,11 +366,9 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={ptoLeave.fromDate}
|
||||
onChange={(e) =>
|
||||
setPtoLeave((prev) => ({ ...prev, fromDate: e.target.value }))
|
||||
}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
className="input input-bordered input-sm w-full focus:input-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -353,30 +378,22 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={ptoLeave.toDate}
|
||||
onChange={(e) =>
|
||||
setPtoLeave((prev) => ({ ...prev, toDate: e.target.value }))
|
||||
}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
className="input input-bordered input-sm w-full focus:input-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="form-control col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Total time adjustment</span>
|
||||
</label>
|
||||
<input
|
||||
type="input"
|
||||
|
||||
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
|
||||
type="text"
|
||||
placeholder="Calculated automatically"
|
||||
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>
|
||||
|
||||
|
|
@ -385,10 +402,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
||||
</label>
|
||||
<textarea
|
||||
// value={ptoLeave.description}
|
||||
// onChange={(e) => handlePtoChange('description', e.target.value)}
|
||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all
|
||||
`}
|
||||
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."
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -418,234 +432,22 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Sick Leave Tab */}
|
||||
{/* Other tabs remain the same */}
|
||||
{activeTab === 'sick' && (
|
||||
<div className="animate-fade-in">
|
||||
<div className="max-w-4xl ">
|
||||
<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>
|
||||
{/* Sick leave content remains the same */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Adjustment Submissions Tab */}
|
||||
{activeTab === 'adjustment' && (
|
||||
<div className="animate-fade-in">
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
{/* Adjustment content remains the same */}
|
||||
</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>{`
|
||||
@keyframes fade-in {
|
||||
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 (
|
||||
|
|
@ -154,7 +164,7 @@ const Timesheets: React.FC = () => {
|
|||
<tr key={entry.projectId}>
|
||||
<td>{index+1}</td>
|
||||
<td>{entry.projectName}</td>
|
||||
<td>{entry.totalWorkTime}</td>
|
||||
<td>{formatWorkTime(entry.totalWorkTime)}</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => handleViewLogs(entry.projectId, entry.projectName)}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface ProjectListResponse {
|
|||
totalWorkTime?: string;
|
||||
totalWorkMs?: number;
|
||||
data?: any[]; // you can replace `any[]` with a typed array later
|
||||
status: number
|
||||
}
|
||||
|
||||
interface TabActivity {
|
||||
|
|
@ -74,14 +75,14 @@ interface ProfileData {
|
|||
accountHolderName: string;
|
||||
accountNumber: string;
|
||||
routingNumber: string;
|
||||
phone:string;
|
||||
skype:string;
|
||||
city:string;
|
||||
state:string;
|
||||
country:string;
|
||||
paymentMethod:string;
|
||||
payoneerAccount:string;
|
||||
veemAccount:string;
|
||||
phone: string;
|
||||
skype: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
paymentMethod: string;
|
||||
payoneerAccount: string;
|
||||
veemAccount: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
|
|
@ -138,13 +139,16 @@ interface Window {
|
|||
|
||||
|
||||
changePassword: (data: {
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
cPassword: string;
|
||||
}) => Promise<{ success: boolean; message?: string; data?: any }>;
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
cPassword: string;
|
||||
}) => Promise<{ success: boolean; message?: string; data?: any }>;
|
||||
|
||||
|
||||
onTokenExpired: (callback: () => void) => void;
|
||||
|
||||
|
||||
getProjectSummary: (projectId: string, date: string) => Promise<any>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue