Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker
This commit is contained in:
commit
f499cbfa7b
|
|
@ -0,0 +1,132 @@
|
||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||||
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||||
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||||
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||||
|
function step(op) {
|
||||||
|
if (f) throw new TypeError("Generator is already executing.");
|
||||||
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||||
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||||
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||||
|
switch (op[0]) {
|
||||||
|
case 0: case 1: t = op; break;
|
||||||
|
case 4: _.label++; return { value: op[1], done: false };
|
||||||
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||||
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||||
|
default:
|
||||||
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||||
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||||
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||||
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||||
|
if (t[2]) _.ops.pop();
|
||||||
|
_.trys.pop(); continue;
|
||||||
|
}
|
||||||
|
op = body.call(thisArg, _);
|
||||||
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||||
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var test_1 = require("@playwright/test");
|
||||||
|
var electronApp;
|
||||||
|
var mainPage;
|
||||||
|
function waitForPreloadScript() {
|
||||||
|
return __awaiter(this, void 0, void 0, function () {
|
||||||
|
var _this = this;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
return [2 /*return*/, new Promise(function (resolve) {
|
||||||
|
var interval = setInterval(function () { return __awaiter(_this, void 0, void 0, function () {
|
||||||
|
var electronBridge;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, mainPage.evaluate(function () {
|
||||||
|
return window.electron;
|
||||||
|
})];
|
||||||
|
case 1:
|
||||||
|
electronBridge = _a.sent();
|
||||||
|
if (electronBridge) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); }, 100);
|
||||||
|
})];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test_1.test.beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, test_1._electron.launch({
|
||||||
|
args: ['.'],
|
||||||
|
env: { NODE_ENV: 'development' },
|
||||||
|
})];
|
||||||
|
case 1:
|
||||||
|
electronApp = _a.sent();
|
||||||
|
return [4 /*yield*/, electronApp.firstWindow()];
|
||||||
|
case 2:
|
||||||
|
mainPage = _a.sent();
|
||||||
|
return [4 /*yield*/, waitForPreloadScript()];
|
||||||
|
case 3:
|
||||||
|
_a.sent();
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
test_1.test.afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, electronApp.close()];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
(0, test_1.test)('custom frame should minimize the mainWindow', function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||||
|
var isMinimized;
|
||||||
|
return __generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0: return [4 /*yield*/, mainPage.click('#minimize')];
|
||||||
|
case 1:
|
||||||
|
_a.sent();
|
||||||
|
return [4 /*yield*/, electronApp.evaluate(function (electron) {
|
||||||
|
return electron.BrowserWindow.getAllWindows()[0].isMinimized();
|
||||||
|
})];
|
||||||
|
case 2:
|
||||||
|
isMinimized = _a.sent();
|
||||||
|
(0, test_1.expect)(isMinimized).toBeTruthy();
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
(0, test_1.test)('should create a custom menu', function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||||
|
var menu;
|
||||||
|
var _a, _b;
|
||||||
|
return __generator(this, function (_c) {
|
||||||
|
switch (_c.label) {
|
||||||
|
case 0: return [4 /*yield*/, electronApp.evaluate(function (electron) {
|
||||||
|
return electron.Menu.getApplicationMenu();
|
||||||
|
})];
|
||||||
|
case 1:
|
||||||
|
menu = _c.sent();
|
||||||
|
(0, test_1.expect)(menu).not.toBeNull();
|
||||||
|
(0, test_1.expect)(menu === null || menu === void 0 ? void 0 : menu.items).toHaveLength(2);
|
||||||
|
(0, test_1.expect)((_a = menu === null || menu === void 0 ? void 0 : menu.items[0].submenu) === null || _a === void 0 ? void 0 : _a.items).toHaveLength(2);
|
||||||
|
(0, test_1.expect)((_b = menu === null || menu === void 0 ? void 0 : menu.items[1].submenu) === null || _b === void 0 ? void 0 : _b.items).toHaveLength(3);
|
||||||
|
(0, test_1.expect)(menu === null || menu === void 0 ? void 0 : menu.items[1].label).toBe('View');
|
||||||
|
return [2 /*return*/];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); });
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use strict";
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var test_1 = require("@playwright/test");
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
exports.default = (0, test_1.defineConfig)({
|
||||||
|
testDir: './e2e',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
// baseURL: 'http://127.0.0.1:3000',
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: __assign({}, test_1.devices['Desktop Chrome']),
|
||||||
|
},
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev:react',
|
||||||
|
url: 'http://localhost:5123',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -81,7 +81,7 @@ ipcMain.handle("get-project-list", async () => {
|
||||||
|
|
||||||
// console.log(" API /list/project response:", res);
|
// console.log(" API /list/project response:", res);
|
||||||
|
|
||||||
// ✅ 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);
|
||||||
|
|
@ -179,7 +179,7 @@ ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }
|
||||||
|
|
||||||
let url = `/report/project/${projectID}`;
|
let url = `/report/project/${projectID}`;
|
||||||
|
|
||||||
console.log("url" , url)
|
|
||||||
|
|
||||||
// 🧠 Append query params only if dates are provided
|
// 🧠 Append query params only if dates are provided
|
||||||
if (fromDate && toDate) {
|
if (fromDate && toDate) {
|
||||||
|
|
@ -190,16 +190,33 @@ ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("url" , url)
|
||||||
|
|
||||||
const response = await api.get(url);
|
const response = await api.get(url);
|
||||||
console.log("✅ response-logs:", response);
|
console.log("✅ response-logs:", response);
|
||||||
|
|
||||||
return response;
|
return { success: true, data: response.data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error fetching project logs:", error);
|
console.error("❌ Error fetching project logs:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("forgot-password", async (event, email: string) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post("/forgot-password", { email });
|
||||||
|
console.log(response , "Line response")
|
||||||
|
if (response) {
|
||||||
|
return { success: true, message: response.data?.message };
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Forgot password error:", error.response?.data || error.message);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.response?.data?.message || "Something went wrong",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
ipcMain.handle('get-running-apps', async () => {
|
ipcMain.handle('get-running-apps', async () => {
|
||||||
return await getRunningAppsList()
|
return await getRunningAppsList()
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function getMainWindow() {
|
export function getMainWindow() {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var electron_1 = require("electron");
|
||||||
|
console.log("setting PreLoad");
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('electron', {
|
||||||
|
login: function (credentials) { return electron_1.ipcRenderer.invoke('login-request', credentials); },
|
||||||
|
logout: function () { return electron_1.ipcRenderer.invoke('logout-request'); },
|
||||||
|
forgotPassword: function (email) { return electron_1.ipcRenderer.invoke("forgot-password", email); },
|
||||||
|
getToken: function (TOKEN_KEY) { return electron_1.ipcRenderer.invoke('get-token', TOKEN_KEY); },
|
||||||
|
uploadScreenshot: function () { return electron_1.ipcRenderer.invoke('upload-screenshot'); },
|
||||||
|
getActiveAppDetails: function () { return electron_1.ipcRenderer.invoke('get-active-app'); },
|
||||||
|
toggleCloseBtn: function (shouldEnable) { return electron_1.ipcRenderer.invoke('toggle-close-btn', shouldEnable); },
|
||||||
|
resizeWindow: function (width, height) { return electron_1.ipcRenderer.invoke('resize-window', width, height); },
|
||||||
|
getProjectList: function () { return electron_1.ipcRenderer.invoke("get-project-list"); },
|
||||||
|
onWindowUpdate: function (callback) {
|
||||||
|
return electron_1.ipcRenderer.on("window:updated", function (_, data) { return callback(data); });
|
||||||
|
},
|
||||||
|
startProjectTracking: function (data) { return electron_1.ipcRenderer.invoke("start-project-tracking", data); },
|
||||||
|
stopProjectTracking: function (data) { return electron_1.ipcRenderer.invoke("stop-project-tracking", data); },
|
||||||
|
trackingTabsOpeningClosing: function (data) {
|
||||||
|
return electron_1.ipcRenderer.invoke("tracking-tabs-opening-and-closing", data);
|
||||||
|
},
|
||||||
|
getProjectLogs: function (projectID, fromDate, toDate) { return electron_1.ipcRenderer.invoke("get-project-logs", { projectID: projectID, fromDate: fromDate, toDate: toDate }); },
|
||||||
|
});
|
||||||
|
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||||
|
// key: Key
|
||||||
|
// ): Promise<EventPayloadMapping[Key]> {
|
||||||
|
// return ipcRenderer.invoke(key);
|
||||||
|
// }
|
||||||
|
// function ipcOn<Key extends keyof EventPayloadMapping>(
|
||||||
|
// key: Key,
|
||||||
|
// callback: (payload: EventPayloadMapping[Key]) => void
|
||||||
|
// ) {
|
||||||
|
// const cb = (_: Electron.IpcRendererEvent, payload: any) => callback(payload);
|
||||||
|
// ipcRenderer.on(key, cb);
|
||||||
|
// return () => ipcRenderer.off(key, cb);
|
||||||
|
// }
|
||||||
|
// function ipcSend<Key extends keyof EventPayloadMapping>(
|
||||||
|
// key: Key,
|
||||||
|
// payload: EventPayloadMapping[Key]
|
||||||
|
// ) {
|
||||||
|
// ipcRenderer.send(key, payload);
|
||||||
|
// }
|
||||||
|
|
@ -5,6 +5,7 @@ console.log("setting PreLoad")
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
login: (credentials: { email: string, password: string }) => ipcRenderer.invoke('login-request', credentials),
|
login: (credentials: { email: string, password: string }) => ipcRenderer.invoke('login-request', credentials),
|
||||||
logout: () => ipcRenderer.invoke('logout-request'),
|
logout: () => ipcRenderer.invoke('logout-request'),
|
||||||
|
forgotPassword: (email: string) => ipcRenderer.invoke("forgot-password", email),
|
||||||
getToken: (TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => ipcRenderer.invoke('get-token', TOKEN_KEY),
|
getToken: (TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => ipcRenderer.invoke('get-token', TOKEN_KEY),
|
||||||
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
|
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
|
||||||
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
|
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,38 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { AppDispatch, RootState } from "./store";
|
||||||
|
import { loadUserFromStorage } from "./store/userSlice";
|
||||||
|
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import Projects from "./pages/Projects/Projects";
|
import Projects from "./pages/Projects/Projects";
|
||||||
import DashboardLayout from "./layout/DashboardLayout";
|
import DashboardLayout from "./layout/DashboardLayout";
|
||||||
import ProtectedRoute from "./routes/ProtectedRoute"
|
import ProtectedRoute from "./routes/ProtectedRoute";
|
||||||
import PublicRoute from "./routes/PublicRoute";
|
import PublicRoute from "./routes/PublicRoute";
|
||||||
import { ThemeToggle } from "./helper/ThemeToggle"
|
import { ThemeToggle } from "./helper/ThemeToggle";
|
||||||
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
||||||
import Timesheets from "./pages/Timesheets/Timesheets";
|
import Timesheets from "./pages/Timesheets/Timesheets";
|
||||||
import Settings from "./pages/Settings/Settings";
|
import Settings from "./pages/Settings/Settings";
|
||||||
import HelpSupport from "./pages/HelpSupport/HelpSupport";
|
import HelpSupport from "./pages/HelpSupport/HelpSupport";
|
||||||
|
|
||||||
interface User {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const theme = ThemeToggle();
|
const theme = ThemeToggle();
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
// ✅ Get user directly from Redux
|
||||||
|
const user = useSelector((state: RootState) => state.user.user);
|
||||||
|
|
||||||
// Apply theme
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
// Load user from localStorage
|
// ✅ Load user from localStorage into Redux when app starts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem("user");
|
dispatch(loadUserFromStorage());
|
||||||
if (storedUser) {
|
}, [dispatch]);
|
||||||
try {
|
|
||||||
setUser(JSON.parse(storedUser));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Invalid user in localStorage", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log("App.tsx → user:", user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
@ -50,7 +42,7 @@ const App: React.FC = () => {
|
||||||
path="/login"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
<PublicRoute user={user}>
|
<PublicRoute user={user}>
|
||||||
<Login setUser={setUser} />
|
<Login />
|
||||||
</PublicRoute>
|
</PublicRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -60,7 +52,7 @@ const App: React.FC = () => {
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute user={user}>
|
<ProtectedRoute user={user}>
|
||||||
<DashboardLayout user={user} setUser={setUser} />
|
<DashboardLayout />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ export const useActiveAppTracker = (
|
||||||
isTracking: boolean,
|
isTracking: boolean,
|
||||||
activeProjectLogId: string | null,
|
activeProjectLogId: string | null,
|
||||||
projectId: string | null,
|
projectId: string | null,
|
||||||
projectName: string
|
projectName: string | null
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const lastActiveRef = useRef<WindowInfo | null>(null);
|
const lastActiveRef = useRef<WindowInfo | null>(null);
|
||||||
const lastLogIdRef = useRef<string | null>(null);
|
const lastLogIdRef = useRef<string | null>(null);
|
||||||
const lastTitleRef = useRef<string | null>(null);
|
const lastTitleRef = useRef<string | null>(null);
|
||||||
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalIdRef = useRef<number | null>(null);
|
||||||
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
|
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
|
||||||
|
|
||||||
// ✅ Common guard to check if we can send
|
// ✅ Common guard to check if we can send
|
||||||
|
|
@ -31,15 +31,27 @@ export const useActiveAppTracker = (
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload: {
|
||||||
id: !isNew ? lastLogIdRef.current : undefined,
|
id?: string;
|
||||||
|
logId: string;
|
||||||
|
projectId: string;
|
||||||
|
title: string;
|
||||||
|
appName: string;
|
||||||
|
} = {
|
||||||
logId: projectLogId,
|
logId: projectLogId,
|
||||||
projectId,
|
projectId,
|
||||||
title,
|
title,
|
||||||
appName,
|
appName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ only attach `id` if it’s non-null and not undefined
|
||||||
|
if (!isNew && lastLogIdRef.current) {
|
||||||
|
payload.id = lastLogIdRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
|
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
|
||||||
|
|
||||||
const res = await window.electron.trackingTabsOpeningClosing(payload);
|
const res = await window.electron.trackingTabsOpeningClosing(payload);
|
||||||
|
|
||||||
if (isNew && res?._id) lastLogIdRef.current = res._id;
|
if (isNew && res?._id) lastLogIdRef.current = res._id;
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,17 @@ import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "../store";
|
import { RootState } from "../store";
|
||||||
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
|
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
|
||||||
|
import { clearUser } from "../store/userSlice";
|
||||||
|
|
||||||
|
// interface User {
|
||||||
|
// email: string;
|
||||||
|
// password: string;
|
||||||
|
// }
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated Project interface to match the JSON structure
|
// Updated Project interface to match the JSON structure
|
||||||
|
|
@ -72,12 +79,7 @@ export interface TrackingState {
|
||||||
projectsData: Project[];
|
projectsData: Project[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
const DashboardLayout = () => {
|
||||||
user: User | null;
|
|
||||||
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -119,8 +121,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const { finalizeTracking } = useActiveAppTracker(
|
const { finalizeTracking } = useActiveAppTracker(
|
||||||
isTracking,
|
isTracking,
|
||||||
activeProjectLogId,
|
activeProjectLogId,
|
||||||
selectedProject?.projectId,
|
selectedProject?.projectId ?? null,
|
||||||
selectedProject?.projectName
|
selectedProject?.projectName ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
const socketRef = useRef<Socket | null>(null);
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
|
@ -174,23 +176,28 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
setSelectedProject(firstProject);
|
setSelectedProject(firstProject);
|
||||||
dispatch(setSelectedProjectSlice(firstProject));
|
dispatch(setSelectedProjectSlice(firstProject));
|
||||||
|
|
||||||
|
// Convert totalWorkMs to seconds for elapsedTime
|
||||||
|
const initialElapsedTime = Math.floor(firstProject.totalWorkMs / 1000);
|
||||||
|
|
||||||
// Initialize tracking map for that project
|
// Initialize tracking map for that project
|
||||||
setProjectTrackingMap((prev) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
if (!updated.has(firstProject.projectId)) {
|
if (!updated.has(firstProject.projectId)) {
|
||||||
updated.set(firstProject.projectId, {
|
updated.set(firstProject.projectId, {
|
||||||
elapsedTime: 0,
|
elapsedTime:initialElapsedTime,
|
||||||
startTime: null,
|
startTime: null,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
pauseStartTime: null,
|
pauseStartTime: null,
|
||||||
totalPausedTime: 0,
|
totalPausedTime: 0,
|
||||||
accumulatedTime: 0,
|
accumulatedTime: initialElapsedTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
// Set the initial elapsed time in the component state
|
||||||
|
setElapsedTime(initialElapsedTime);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ Unexpected response:", res);
|
console.warn("⚠️ Unexpected response:", res);
|
||||||
|
|
@ -210,12 +217,89 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await window.electron.logout();
|
const res = await window.electron.logout();
|
||||||
localStorage.removeItem("user");
|
if (res) {
|
||||||
setUser(null);
|
dispatch(clearUser()); // ✅ Removes from Redux + localStorage
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout error:", error);
|
console.error("Logout error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect and start sending
|
||||||
|
const handleConnect = async () => {
|
||||||
|
|
||||||
|
const token = await window.electron.getToken("ACCESS_TOKEN");
|
||||||
|
|
||||||
|
if (socketRef.current) {
|
||||||
|
console.warn("Already connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!selectedProject?.projectId) return
|
||||||
|
|
||||||
|
console.log(selectedProject?.projectId, "SelectedProject ID")
|
||||||
|
socketRef.current = io("https://wired-terminally-muskrat.ngrok-free.app", {
|
||||||
|
transports: ["websocket"],
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
token: token,
|
||||||
|
projectId: selectedProject?.projectId,
|
||||||
|
userType: "va"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.on("connect", () => {
|
||||||
|
console.log("🟢 Connected:", socketRef.current?.id);
|
||||||
|
setIsConnected(true);
|
||||||
|
|
||||||
|
// Start sending every 5 seconds
|
||||||
|
intervalRef.current = setInterval(async () => {
|
||||||
|
const apps = await window.electron.getRunningApps()
|
||||||
|
socketRef.current?.emit("getVisibleApps", { token: await token, apps });
|
||||||
|
|
||||||
|
// const base64String = await window.electron.captureAppScreenShot("Screen 1");
|
||||||
|
// if (base64String) {
|
||||||
|
// socketRef.current?.emit("sendImage", {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5MGIzMjhkMDkxMDFmNzRjZjUyNGMwNyIsImVtYWlsIjoiYmhhZ3dhbi5yQGhpY2dsb2JhbHNvbHV0aW9ucy5jb20iLCJuYW1lIjoiUm9oaXQiLCJpYXQiOjE3NjI4NTgxOTMsImV4cCI6MTc2Mjk0NDU5M30.UlHy3RU9W5LOid7smG7WzyNT7dnqhQT4qqzRrN1axmU", image: base64String, title: "Screen 1"});
|
||||||
|
// }
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.on("appToCapture", async ({ appTitle }) => {
|
||||||
|
console.log("📸 Capture requested for:", appTitle);
|
||||||
|
|
||||||
|
const base64String = await window.electron.captureAppScreenShot(appTitle);
|
||||||
|
if (base64String && socketRef.current) {
|
||||||
|
socketRef.current.emit("sendImage", {
|
||||||
|
token: token,
|
||||||
|
image: base64String,
|
||||||
|
title: appTitle
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.on("disconnect", () => {
|
||||||
|
console.log("🔴 Disconnected");
|
||||||
|
setIsConnected(false);
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
setIsConnected(false);
|
||||||
|
console.log("🔌 Disconnected manually");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect and start sending
|
// Connect and start sending
|
||||||
|
|
@ -514,29 +598,53 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
|
|
||||||
// Handle project selection
|
// Handle project selection
|
||||||
const handleProjectSelect = (projectId: string) => {
|
const handleProjectSelect = (projectId: string) => {
|
||||||
if (isTracking) return;
|
if (isTracking) return; // prevent switching while tracking
|
||||||
|
|
||||||
const project = projectsData.find((p) => p.projectId === projectId);
|
const project = projectsData.find((p) => p.projectId === projectId);
|
||||||
if (project) {
|
if (!project) return;
|
||||||
setSelectedProject(project);
|
|
||||||
|
|
||||||
const projectData = projectTrackingMap.get(projectId);
|
setSelectedProject(project);
|
||||||
if (projectData) {
|
dispatch(setSelectedProjectSlice(project));
|
||||||
setElapsedTime(projectData.elapsedTime);
|
|
||||||
setStartTime(projectData.startTime);
|
const existingData = projectTrackingMap.get(projectId);
|
||||||
setEndTime(projectData.endTime);
|
|
||||||
setIsTracking(projectData.isTracking);
|
setProjectTrackingMap((prev) => {
|
||||||
setIsPaused(projectData.isPaused);
|
const updated = new Map(prev);
|
||||||
} else {
|
|
||||||
setElapsedTime(0);
|
// If project is not yet tracked, initialize it with totalWorkMs
|
||||||
|
if (!existingData) {
|
||||||
|
const initialElapsedTime = Math.floor((project.totalWorkMs || 0) / 1000);
|
||||||
|
updated.set(projectId, {
|
||||||
|
elapsedTime: initialElapsedTime,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
isTracking: false,
|
||||||
|
isPaused: false,
|
||||||
|
pauseStartTime: null,
|
||||||
|
totalPausedTime: 0,
|
||||||
|
accumulatedTime: initialElapsedTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update component-level state
|
||||||
|
setElapsedTime(initialElapsedTime);
|
||||||
setStartTime(null);
|
setStartTime(null);
|
||||||
setEndTime(null);
|
setEndTime(null);
|
||||||
setIsTracking(false);
|
setIsTracking(false);
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
|
} else {
|
||||||
|
// If already tracked, restore its state
|
||||||
|
setElapsedTime(existingData.elapsedTime);
|
||||||
|
setStartTime(existingData.startTime);
|
||||||
|
setEndTime(existingData.endTime);
|
||||||
|
setIsTracking(existingData.isTracking);
|
||||||
|
setIsPaused(existingData.isPaused);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const trackingState: TrackingState = {
|
const trackingState: TrackingState = {
|
||||||
selectedProject,
|
selectedProject,
|
||||||
projectTrackingMap,
|
projectTrackingMap,
|
||||||
|
|
@ -654,11 +762,11 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* Layout Body */}
|
{/* Layout Body */}
|
||||||
<div className={`flex ${!isTracking ? "max-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4">
|
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item , index) => (
|
||||||
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
|
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label} key={index}>
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.label}
|
key={item.label}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
|
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
import NoData from "./NoData";
|
||||||
|
|
||||||
// Updated Project interface to match JSON structure
|
// Updated Project interface to match JSON structure
|
||||||
interface Project {
|
interface Project {
|
||||||
|
|
@ -103,44 +104,18 @@ const Dashboard: React.FC = () => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 overflow-y-auto no-scrollbar">
|
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h1 className="text-md font-bold">Dashboard</h1>
|
<h1 className="text-md font-bold">Dashboard</h1>
|
||||||
<button
|
{/* <button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await window.electron.uploadScreenshot();
|
await window.electron.uploadScreenshot();
|
||||||
}}
|
}}
|
||||||
className="btn btn-xs btn-outline mt-2"
|
className="btn btn-xs btn-outline mt-2"
|
||||||
>
|
>
|
||||||
Screenshot Test
|
Screenshot Test
|
||||||
</button>
|
</button> */}
|
||||||
{/* {
|
|
||||||
runningApps && runningApps.map((obj) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={obj.pid}
|
|
||||||
onClick={async () => {
|
|
||||||
console.log("Call ", obj.name)
|
|
||||||
await window.electron.captureAppScreenShot(obj.name)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{obj.name}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} */}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widgets */}
|
{/* Widgets */}
|
||||||
|
|
@ -171,18 +146,25 @@ const Dashboard: React.FC = () => {
|
||||||
<span className="text-md font-bold">Tracking</span>
|
<span className="text-md font-bold">Tracking</span>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="card bg-base-100 border border-base-200 shadow-sm">
|
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{selectedProject ? (
|
{selectedProject ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
<p className="text-lg font-extrabold text-success">
|
<p className="text-lg font-extrabold text-success">
|
||||||
{formatTime(selectedData?.elapsedTime || 0)}
|
{formatTime(selectedData?.elapsedTime || 0)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-base-content/70 mt-1">
|
{/* <p className="text-xs text-base-content/70 mt-1">
|
||||||
{selectedData?.isTracking ? "Current Session" : "Ready"}
|
{selectedData?.isTracking ? "Current Session" : "Ready"}
|
||||||
</p>
|
</p> */}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-base-content/70">Project:</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{selectedProject.projectName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!selectedData?.isTracking ? (
|
{!selectedData?.isTracking ? (
|
||||||
|
|
@ -203,15 +185,9 @@ const Dashboard: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider my-3" />
|
<div className="divider my-1" />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
{/* <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
||||||
<div>
|
|
||||||
<span className="text-base-content/70">Project:</span>{" "}
|
|
||||||
<span className="font-semibold">
|
|
||||||
{selectedProject.projectName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base-content/70">Status:</span>{" "}
|
<span className="text-base-content/70">Status:</span>{" "}
|
||||||
<span
|
<span
|
||||||
|
|
@ -248,7 +224,7 @@ const Dashboard: React.FC = () => {
|
||||||
: "—"}
|
: "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-base-content/60 text-center">
|
<p className="text-sm text-base-content/60 text-center">
|
||||||
|
|
@ -260,12 +236,22 @@ const Dashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Projects Table */}
|
{/* Projects Table */}
|
||||||
<div>
|
<div className=" border border-base-content/10 bg-base-100 shadow-md min-h-[58vh] flex flex-col">
|
||||||
<p className="text-md mb-2 font-bold">Projects</p>
|
{/* 🔹 Sticky Header */}
|
||||||
<div className="relative rounded-box border border-base-content/10 bg-base-100 shadow-md min-h-[62vh] max-h-[62vh]">
|
<div className=" sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
|
||||||
|
<p className="text-md font-bold">Projects</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🔹 Table or Empty State */}
|
||||||
{projectsData.length > 0 ? (
|
{projectsData.length > 0 ? (
|
||||||
<div className="overflow-y-auto h-full">
|
<div
|
||||||
<table className="table table-zebra w-full text-sm">
|
className="overflow-y-auto"
|
||||||
|
style={{
|
||||||
|
maxHeight: "50vh", // scroll only after this height
|
||||||
|
minHeight: "auto", // auto adjusts if few rows
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<table className="table table-zebra w-full text-[12px]">
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
<thead className="sticky top-0 bg-base-200 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
|
|
@ -276,14 +262,17 @@ const Dashboard: React.FC = () => {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{projectsData.map((project, index) => {
|
{projectsData.map((project, index) => {
|
||||||
const isCurrentlySelected = selectedProject?.projectId === project.projectId;
|
const isCurrentlySelected =
|
||||||
|
selectedProject?.projectId === project.projectId;
|
||||||
const isDisabled = isTracking && !isCurrentlySelected;
|
const isDisabled = isTracking && !isCurrentlySelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={project.projectId}>
|
<tr key={project.projectId}>
|
||||||
<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">{project.totalWorkTime}</td>
|
<td className="font-mono text-xs">
|
||||||
|
{project.totalWorkTime || "0h 0m 0s"}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
|
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
|
||||||
|
|
@ -301,13 +290,13 @@ const Dashboard: React.FC = () => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-sm text-base-content/60">
|
<div className="flex items-center justify-center flex-1 text-sm text-base-content/60">
|
||||||
<p className="mt-10 font-bold text-lg">No projects found.</p>
|
<NoData/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ 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>>({});
|
||||||
|
|
||||||
|
|
@ -49,6 +50,7 @@ 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("")
|
||||||
|
|
@ -80,9 +82,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
errors.toDate = 'To date must be after from date';
|
errors.toDate = 'To date must be after from date';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.description.trim()) {
|
|
||||||
errors.description = 'Description/Reason is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
@ -100,7 +99,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
console.log('Submitting PTO:', ptoLeave);
|
console.log('Submitting PTO:', ptoLeave);
|
||||||
alert('PTO request submitted successfully!');
|
alert('PTO request submitted successfully!');
|
||||||
|
|
||||||
setPtoLeave({ fromDate: '', toDate: '', description: '' });
|
setPtoLeave({ fromDate: '', toDate: '' });
|
||||||
setPtoErrors({});
|
setPtoErrors({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Failed to submit request. Please try again.');
|
alert('Failed to submit request. Please try again.');
|
||||||
|
|
@ -128,7 +127,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
console.log('Submitting Sick Leave:', sickLeave);
|
console.log('Submitting Sick Leave:', sickLeave);
|
||||||
alert('Sick Leave request submitted successfully!');
|
alert('Sick Leave request submitted successfully!');
|
||||||
|
|
||||||
setSickLeave({ fromDate: '', toDate: '', description: '' });
|
setSickLeave({ fromDate: '', toDate: '' });
|
||||||
setSickErrors({});
|
setSickErrors({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Failed to submit request. Please try again.');
|
alert('Failed to submit request. Please try again.');
|
||||||
|
|
@ -173,35 +172,35 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
// const fetchLogs = async () => {
|
||||||
const res = await window.electron.getProjectLogs(
|
// const res = await window.electron.getProjectLogs(
|
||||||
selectedProjectId,
|
// selectedProjectId,
|
||||||
selectedDate || undefined,
|
// selectedDate || undefined,
|
||||||
selectedDate || undefined,
|
// selectedDate || undefined,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (res.success) {
|
// if (res.success) {
|
||||||
console.log("✅ Logs: fetchLogs", res);
|
// console.log("✅ Logs: fetchLogs", res);
|
||||||
|
|
||||||
// Safely extract time if from/to exist
|
// // Safely extract time if from/to exist
|
||||||
const fromTime = res.from ? new Date(res.from).toISOString().slice(11, 16) : "";
|
// const fromTime = res.from ? new Date(res.from).toISOString().slice(11, 16) : "";
|
||||||
const toTime = res.to ? new Date(res.to).toISOString().slice(11, 16) : "";
|
// const toTime = res.to ? new Date(res.to).toISOString().slice(11, 16) : "";
|
||||||
|
|
||||||
setPtoLeave({
|
// setPtoLeave({
|
||||||
fromDate: fromTime,
|
// fromDate: fromTime,
|
||||||
toDate: toTime,
|
// toDate: toTime,
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
console.warn("⚠️ Failed to fetch project logs");
|
// console.warn("⚠️ Failed to fetch project logs");
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (selectedProjectId && selectedDate) {
|
// if (selectedProjectId && selectedDate) {
|
||||||
fetchLogs()
|
// fetchLogs()
|
||||||
}
|
// }
|
||||||
}, [selectedProjectId, selectedDate])
|
// }, [selectedProjectId, selectedDate])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -249,7 +248,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[80vh]">
|
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
|
||||||
{/* PTO Tab */}
|
{/* PTO Tab */}
|
||||||
{activeTab === 'create_and_adjust_timesheet' && (
|
{activeTab === 'create_and_adjust_timesheet' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
|
|
@ -386,16 +385,12 @@ 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}
|
// value={ptoLeave.description}
|
||||||
// onChange={(e) => handlePtoChange('description', e.target.value)}
|
// onChange={(e) => handlePtoChange('description', e.target.value)}
|
||||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
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."
|
||||||
/>
|
/>
|
||||||
{ptoErrors.description && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error font-medium">{ptoErrors.description}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|
@ -410,7 +405,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPtoLeave({ fromDate: '', toDate: '', description: '' });
|
setPtoLeave({ fromDate: '', toDate: '' });
|
||||||
setPtoErrors({});
|
setPtoErrors({});
|
||||||
}}
|
}}
|
||||||
className="btn btn-secondary transition-all"
|
className="btn btn-secondary transition-all"
|
||||||
|
|
@ -468,7 +463,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-control flex flex-col gap-1">
|
{/* <div className="form-control flex flex-col gap-1">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-semibold ">Description / Reason</span>
|
<span className="label-text font-semibold ">Description / Reason</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -483,7 +478,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
<span className="label-text-alt text-error font-medium">{sickErrors.description}</span>
|
<span className="label-text-alt text-error font-medium">{sickErrors.description}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="flex gap-3 ">
|
<div className="flex gap-3 ">
|
||||||
<button
|
<button
|
||||||
|
|
@ -497,7 +492,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSickLeave({ fromDate: '', toDate: '', description: '' });
|
// setSickLeave({ fromDate: '', toDate: '', description: '' });
|
||||||
setSickErrors({});
|
setSickErrors({});
|
||||||
}}
|
}}
|
||||||
className="btn btn-secondary transition-all"
|
className="btn btn-secondary transition-all"
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,72 @@
|
||||||
import React, { useState, useEffect, useRef, FormEvent, ChangeEvent } from 'react'
|
import React, { useState, useEffect, useRef, FormEvent, ChangeEvent } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { setUserSlice } from '../store/userSlice';
|
||||||
|
|
||||||
interface User {
|
const Login = () => {
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginProps {
|
|
||||||
setUser: (user: User) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Login: React.FC<LoginProps> = ({ setUser }) => {
|
|
||||||
const [formData, setFormData] = useState<User>({ email: '', password: '' })
|
|
||||||
const inputRef = useRef<HTMLFormElement | null>(null)
|
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [errorMessage, setErrorMessage] = useState("")
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isForgotMode, setIsForgotMode] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
|
const handleLogin = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const { email, password } = formData
|
|
||||||
if (email === "" || password === "") return
|
if (email === "" || password === "") return
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
const response = await window.electron.login({ email, password });
|
const response = await window.electron.login({ email, password });
|
||||||
console.log(response)
|
console.log(response)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
localStorage.setItem("user", JSON.stringify(formData));
|
// setUser(response.user);
|
||||||
setUser(response.user);
|
dispatch(setUserSlice({
|
||||||
|
id: response.user.id,
|
||||||
|
name: response.user.name,
|
||||||
|
email: response.user.email,
|
||||||
|
}));
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
} else if (response.success == false) {
|
||||||
|
console.log(response.error.message)
|
||||||
|
setErrorMessage(response.error.message)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleForgotPassword = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await window.electron.forgotPassword(email);
|
||||||
|
console.log("Forgot password response:", res);
|
||||||
|
alert("Password reset link sent to your email");
|
||||||
|
setIsForgotMode(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Forgot password error:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Detect Enter key press
|
// Detect Enter key press
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEnter = (e: KeyboardEvent) => {
|
const handleEnter = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleSubmit(e)
|
if (!isForgotMode) {
|
||||||
|
handleLogin(e)
|
||||||
|
} else {
|
||||||
|
handleForgotPassword(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,9 +74,10 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
inputElement?.addEventListener('keydown', handleEnter)
|
inputElement?.addEventListener('keydown', handleEnter)
|
||||||
|
|
||||||
return () => inputElement?.removeEventListener('keydown', handleEnter)
|
return () => inputElement?.removeEventListener('keydown', handleEnter)
|
||||||
}, [formData])
|
}, [email, password])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
|
||||||
<div className="card w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
|
<div className="card w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
|
@ -77,7 +106,7 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
<form
|
<form
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="flex flex-col items-center justify-center w-full"
|
className="flex flex-col items-center justify-center w-full"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={isForgotMode ? handleForgotPassword : handleLogin}
|
||||||
>
|
>
|
||||||
<div className="form-control w-90 px-5">
|
<div className="form-control w-90 px-5">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -97,14 +126,14 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
className="grow"
|
className="grow"
|
||||||
value={formData.email}
|
value={email}
|
||||||
onChange={handleChange}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-control w-90 px-5 mt-4">
|
{!isForgotMode && <div className="form-control w-90 px-5 mt-3">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-medium">Password</span>
|
<span className="label-text font-medium">Password</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -125,22 +154,51 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
className="grow"
|
className="grow"
|
||||||
value={formData.password}
|
value={password}
|
||||||
onChange={handleChange}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
<div className="form-control w-full px-5 mt-6">
|
<div className="form-control w-90 px-5 mt-4">
|
||||||
<button type="submit" className="btn btn-primary w-full">
|
<button
|
||||||
Sign In
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn btn-primary w-full"
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? "Please wait..."
|
||||||
|
: isForgotMode
|
||||||
|
? "Send Reset Link"
|
||||||
|
: "Login"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
{isForgotMode ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsForgotMode(false)}
|
||||||
|
className="link link-primary text-sm"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsForgotMode(true)}
|
||||||
|
className="link link-primary text-sm"
|
||||||
|
>
|
||||||
|
Forgot Password?
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errorMessage && <span className='text-red-500 font-semibold mt-2'>{errorMessage}...!</span>}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const NoData = ({ message = "No data available", subText = "Please add new items or check back later." }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[67vh] p-6 text-center">
|
||||||
|
{/* SVG Illustration */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 600 360"
|
||||||
|
className="w-64 h-40 mb-6 text-primary"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="38"
|
||||||
|
y="56"
|
||||||
|
width="524"
|
||||||
|
height="208"
|
||||||
|
rx="14"
|
||||||
|
fill="currentColor"
|
||||||
|
opacity="0.05"
|
||||||
|
/>
|
||||||
|
<g transform="translate(125, 68)">
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="24"
|
||||||
|
width="298"
|
||||||
|
height="150"
|
||||||
|
rx="8"
|
||||||
|
fill="white"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
opacity="0.15"
|
||||||
|
/>
|
||||||
|
<g transform="translate(18, 43)" fill="currentColor" opacity="0.1">
|
||||||
|
<rect x="0" y="0" width="160" height="12" rx="6" />
|
||||||
|
<rect x="0" y="24" width="110" height="12" rx="6" />
|
||||||
|
<rect x="0" y="48" width="140" height="12" rx="6" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="translate(160, 72) scale(1.05)"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="10"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="animate-pulse"
|
||||||
|
>
|
||||||
|
<circle cx="24" cy="24" r="18" fill="none" />
|
||||||
|
<path d="M39 39 L58 58" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Text section */}
|
||||||
|
<div className="card bg-base-100 w-full max-w-md">
|
||||||
|
<div className="card-body items-center">
|
||||||
|
<h2 className="card-title text-lg font-semibold text-base-content">
|
||||||
|
{message}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoData;
|
||||||
|
|
@ -1,36 +1,29 @@
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { User, Mail, Lock, Edit2, Shield, CalendarDays } from "lucide-react";
|
import { User, Mail } from "lucide-react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../store";
|
||||||
|
|
||||||
interface UserProfile {
|
interface User {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
|
||||||
employeeId: string;
|
|
||||||
joinDate: string;
|
|
||||||
department: string;
|
|
||||||
profileImage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData: UserProfile = {
|
|
||||||
name: "Jai Pandey",
|
|
||||||
email: "jai.pandey@vatechcorp.com",
|
|
||||||
role: "Frontend Developer",
|
|
||||||
employeeId: "EMP-1023",
|
|
||||||
joinDate: "2022-07-15",
|
|
||||||
department: "Engineering",
|
|
||||||
profileImage: "https://ui-avatars.com/api/?name=Jai+Pandey&background=random",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
|
|
||||||
|
const user = useSelector((state: RootState) => state.user.user);
|
||||||
|
console.log(user , "Line 16")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 max-h-screen">
|
<div className="flex flex-col gap-2 ">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-md font-bold flex items-center gap-2">
|
<h2 className="text-md font-bold flex items-center gap-2">
|
||||||
Profile Settings
|
Profile Settings
|
||||||
</h2>
|
</h2>
|
||||||
<button className="btn btn-sm btn-primary gap-1">
|
<button className="btn btn-sm btn-outline btn-primary">
|
||||||
<Edit2 size={16} /> Edit Profile
|
Change Password
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -43,8 +36,7 @@ const Settings: React.FC = () => {
|
||||||
<User size={60} />
|
<User size={60} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-3 text-lg font-semibold">{userData.name}</h3>
|
<h3 className="mt-3 text-lg font-semibold">{user?.name}</h3>
|
||||||
<p className="text-sm text-base-content/70">{userData.role}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider md:divider-horizontal" />
|
<div className="divider md:divider-horizontal" />
|
||||||
|
|
@ -55,51 +47,8 @@ const Settings: React.FC = () => {
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
||||||
<Mail size={14} /> Email
|
<Mail size={14} /> Email
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-medium">{userData.email}</p>
|
<p className="text-sm font-medium">{user?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<Shield size={14} /> Role
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.role}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<Lock size={14} /> Employee ID
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.employeeId}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<CalendarDays size={14} /> Joining Date
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.joinDate}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50 sm:col-span-2">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<User size={14} /> Department
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.department}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Account Settings */}
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm p-4">
|
|
||||||
<h3 className="text-md font-semibold mb-3">Account Security</h3>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-t border-base-300 pt-3">
|
|
||||||
<p className="text-sm flex items-center gap-2">
|
|
||||||
<Lock size={16} /> Password last changed on{" "}
|
|
||||||
<span className="font-medium text-base-content">Aug 12, 2024</span>
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-sm btn-outline btn-primary">
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
|
||||||
{tab.title} <br/>
|
{tab.title} <br/>
|
||||||
{tab.appName}
|
{tab.appName}
|
||||||
</td>
|
</td>
|
||||||
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
|
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br/><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
|
||||||
<td>{tab.durationFormatted}</td>
|
<td>{tab.durationFormatted}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import windowReducer from "./windowSlice";
|
import windowReducer from "./windowSlice";
|
||||||
import projectsReducer from "./projectSlice";
|
import projectsReducer from "./projectSlice";
|
||||||
|
import userReducer from "./userSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
window: windowReducer,
|
window: windowReducer,
|
||||||
projects: projectsReducer,
|
projects: projectsReducer,
|
||||||
|
user: userReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
// Define and export the User type so other files can use it
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the state shape
|
||||||
|
interface UserState {
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UserState = {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the slice
|
||||||
|
const userSlice = createSlice({
|
||||||
|
name: "user",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setUserSlice: (state, action: PayloadAction<User>) => {
|
||||||
|
state.user = action.payload;
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
localStorage.setItem("user", JSON.stringify(action.payload));
|
||||||
|
},
|
||||||
|
clearUser: (state) => {
|
||||||
|
state.user = null;
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
},
|
||||||
|
loadUserFromStorage: (state) => {
|
||||||
|
const storedUser = localStorage.getItem("user");
|
||||||
|
if (storedUser) {
|
||||||
|
try {
|
||||||
|
state.user = JSON.parse(storedUser) as User;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid user in localStorage:", error);
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export actions and reducer
|
||||||
|
export const { setUserSlice, clearUser, loadUserFromStorage } = userSlice.actions;
|
||||||
|
export default userSlice.reducer;
|
||||||
|
|
@ -49,7 +49,7 @@ const initialState: WindowState = {
|
||||||
interface SetActiveWindowPayload {
|
interface SetActiveWindowPayload {
|
||||||
window: WindowInfo;
|
window: WindowInfo;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowSlice = createSlice({
|
const windowSlice = createSlice({
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ interface Window {
|
||||||
electron: {
|
electron: {
|
||||||
login: (credentials: { email: string; password: string }) => Promise<any>;
|
login: (credentials: { email: string; password: string }) => Promise<any>;
|
||||||
logout: () => Promise<any>;
|
logout: () => Promise<any>;
|
||||||
|
forgotPassword: (email: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
getToken: (tokenKey: "ACCESS_TOKEN" | "REFRESH_TOKEN") => Promise<string | null>;
|
getToken: (tokenKey: "ACCESS_TOKEN" | "REFRESH_TOKEN") => Promise<string | null>;
|
||||||
uploadScreenshot: () => {};
|
uploadScreenshot: () => {};
|
||||||
getActiveAppDetails: () => {};
|
getActiveAppDetails: () => {};
|
||||||
|
|
@ -94,13 +98,16 @@ interface Window {
|
||||||
appName: string;
|
appName: string;
|
||||||
}) => Promise<any>;
|
}) => Promise<any>;
|
||||||
|
|
||||||
// getProjectLogs: (projectId: string) => Promise<{ success: boolean; logs: ProjectLogResponse[] }>;
|
|
||||||
getProjectLogs: (
|
getProjectLogs: (
|
||||||
projectID: string,
|
projectId: string,
|
||||||
fromDate?: string,
|
fromDate?: string,
|
||||||
toDate?: string
|
toDate?: string
|
||||||
) => Promise<{ success: boolean; logs: ProjectLogResponse[] }>;
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data: any;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue