icons setup installer app launcher

This commit is contained in:
JaiPandey425 2025-11-25 12:16:22 +05:30
parent ff32a54aac
commit 880ef9eae5
18 changed files with 443 additions and 235 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
dist dist
dist-react dist-react
dist-electron dist-electron
release
*.local *.local
# Editor directories and files # Editor directories and files

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

View File

@ -1,16 +0,0 @@
{
"appId": "com.n-ziermann.electron-course",
"files": ["dist-electron", "dist-react"],
"extraResources": ["dist-electron/preload.cjs", "src/assets/**"],
"icon": "./desktopIcon.png",
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage",
"category": "Utility"
},
"win": {
"target": ["portable", "msi"]
}
}

95
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "va-tracker", "name": "va-tracker",
"version": "0.0.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "va-tracker", "name": "va-tracker",
"version": "0.0.0", "version": "1.1.1",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.10.1", "@reduxjs/toolkit": "^2.10.1",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
@ -39,6 +39,7 @@
"electron-builder": "^25.0.5", "electron-builder": "^25.0.5",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rcedit": "^5.0.2",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.7", "vite": "^5.4.7",
"vitest": "^2.1.1" "vitest": "^2.1.1"
@ -4578,6 +4579,54 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/cross-spawn-windows-exe": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz",
"integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/malept"
},
{
"type": "tidelift",
"url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund"
}
],
"license": "Apache-2.0",
"dependencies": {
"@malept/cross-spawn-promise": "^1.1.0",
"is-wsl": "^2.2.0",
"which": "^2.0.2"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/cross-spawn-windows-exe/node_modules/@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
"integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/malept"
},
{
"type": "tidelift",
"url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
}
],
"license": "Apache-2.0",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -7377,6 +7426,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-finalizationregistry": { "node_modules/is-finalizationregistry": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
@ -7641,6 +7706,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@ -9749,6 +9827,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/rcedit": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz",
"integrity": "sha512-dgysxaeXZ4snLpPjn8aVtHvZDCx+aRcvZbaWBgl1poU6OPustMvOkj9a9ZqASQ6i5Y5szJ13LSvglEOwrmgUxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn-windows-exe": "^1.1.0"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",

View File

@ -52,6 +52,7 @@
"output": "release", "output": "release",
"buildResources": "build" "buildResources": "build"
}, },
"icon": "build/icon.ico",
"files": [ "files": [
"dist-electron/**/*", "dist-electron/**/*",
"dist-react/**/*", "dist-react/**/*",
@ -71,11 +72,12 @@
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
"arch": ["x64"] "arch": [
"x64"
]
} }
], ],
"icon": "src/assets/icons/appIcon.ico", "signAndEditExecutable": true,
"signAndEditExecutable": false,
"signingHashAlgorithms": [], "signingHashAlgorithms": [],
"verifyUpdateCodeSignature": false "verifyUpdateCodeSignature": false
}, },
@ -85,6 +87,9 @@
"createDesktopShortcut": "always", "createDesktopShortcut": "always",
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"runAfterFinish": true, "runAfterFinish": true,
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico",
"artifactName": "${productName}-Setup-${version}.${ext}" "artifactName": "${productName}-Setup-${version}.${ext}"
} }
}, },
@ -102,6 +107,7 @@
"electron-builder": "^25.0.5", "electron-builder": "^25.0.5",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rcedit": "^5.0.2",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.7", "vite": "^5.4.7",
"vitest": "^2.1.1" "vitest": "^2.1.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 435 KiB

View File

@ -340,6 +340,28 @@ ipcMain.handle("create-timesheet", async (event, data) => {
}) })
ipcMain.handle("create-leave-request", async (event, payload) => {
try {
console.log(" Leave Request Received:", payload);
const response = await api.post("/leave/request", payload);
console.log(" Leave Saved:", response.data);
return { success: true, data: response.data };
} catch (error) {
console.error("❌ Leave Save Failed:", error);
return {
success: false,
error: error || "Unknown error"
};
}
});
export const sendTokenExpired = () => { export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer"); console.log("⚠️ Sending token-expired event to renderer");

View File

@ -23,8 +23,9 @@ function resolveIcon() {
console.log("Line 24", path.join(__dirname, "..", "assets", "icons", "appIcon.ico")) console.log("Line 24", path.join(__dirname, "..", "assets", "icons", "appIcon.ico"))
// PRODUCTION mode → inside dist-electron const prodPath = path.join(process.resourcesPath, "assets", "icons", "appIcon.ico");
return path.join(__dirname, "..", "assets", "icons", "appIcon.ico"); console.log("PROD Icon Path:", prodPath);
return prodPath;
} }
// --------------------------- // ---------------------------

View File

@ -50,6 +50,10 @@ contextBridge.exposeInMainWorld('electron', {
createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data), createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data),
createPTOAndSickLeaveRequest: (data) =>
ipcRenderer.invoke("create-leave-request", data),
} satisfies Window['electron']); } satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>( // function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -65,7 +65,6 @@ export const handleLogin = async (credentials: { email: string, password: string
console.log("UIPATH", getUIPath()); console.log("UIPATH", getUIPath());
console.log("Doom 1"); console.log("Doom 1");
try { try {
console.log("➡️ Calling API..."); console.log("➡️ Calling API...");
const response = await axios.post( const response = await axios.post(
@ -188,3 +187,6 @@ export const captureSpecificApp = async (windowTitle: string): Promise<string> =
return 'Failed to capture screenshot.'; return 'Failed to capture screenshot.';
} }
} }
// timmer stop after 4-5 sec check it..

View File

@ -107,7 +107,7 @@ const DashboardLayout = () => {
const menuItems = [ const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true }, { label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> }, { label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> }, { label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> }, { label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> }, { label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
@ -211,6 +211,54 @@ const DashboardLayout = () => {
// STEP 6: Clear active project log ID // STEP 6: Clear active project log ID
setActiveProjectLogId(""); setActiveProjectLogId("");
// ✨ NEW STEP 7: Refresh project list to get updated work hours
try {
console.log("🔄 Refreshing project list...");
const res = await window.electron.getProjectList();
console.log("📊 Updated project list:", res);
if (res?.success && Array.isArray(res.data)) {
// Update projects data
dispatch(setProjectsDataSlice(res.data));
// Update project summary
dispatch(
setProjectSummarySlice({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
})
);
// Update selected project with fresh data
const updatedProject = res.data.find(
(p: Project) => p.projectId === projectId
);
if (updatedProject) {
dispatch(setSelectedProjectSlice(updatedProject));
// Update tracking map with new total work time
const initialElapsed = Math.floor((updatedProject.totalWorkMs || 0) / 1000);
dispatch(
initializeProjectTracking({
projectId: updatedProject.projectId,
initialTime: initialElapsed,
})
);
console.log("✅ Selected project updated with fresh data");
}
}
} catch (error) {
console.error("❌ Error refreshing project list:", error);
}
setLoadingStop(false); setLoadingStop(false);
console.log("🧹 Active project log ID cleared"); console.log("🧹 Active project log ID cleared");

View File

@ -1,3 +1,4 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store'; import { RootState } from '../../store';
@ -9,23 +10,46 @@ type TabType = 'pto' | 'sick' | 'adjustment';
const LeaveAndAdjustments: React.FC = () => { const LeaveAndAdjustments: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto'); const [activeTab, setActiveTab] = useState<TabType>('pto');
const [isSubmitting, setIsSubmitting] = useState(false); // 🚀 NEW
const projects = useSelector((state: RootState) => state.projects.projectsData); const projects = useSelector((state: RootState) => state.projects.projectsData);
console.log("LeaveAndAdjustments", projects); // ------- COMMON API CALL FOR BOTH TABS -------
const submitLeave = async (payload: {
dateFrom: string;
dateTo: string;
leaveType: "PTO" | "SICK";
comment: string;
}) => {
try {
setIsSubmitting(true);
console.log("Leave Payload:", payload);
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:" ,response)
if (response.success) {
return true;
}
} catch (err) {
console.error(err);
return false;
} finally {
setIsSubmitting(false);
}
};
return ( return (
<div className="max-h-screen"> <div className="max-h-screen">
<div className="w-full mx-auto"> <div className="w-full mx-auto">
{/* Header */} <h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
<div className="">
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
</div>
{/* Tabs Navigation */} {/* Tabs — Disabled while submitting */}
<div role="tablist" className="tabs tabs-border w-full mb-1"> <div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100">
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""}`} className={`tab text-xs font-semibold ${
activeTab === "pto" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("pto")} onClick={() => setActiveTab("pto")}
> >
PTO Leave PTO Leave
@ -33,7 +57,9 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""}`} className={`tab text-xs font-semibold ${
activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")} onClick={() => setActiveTab("sick")}
> >
Sick Leave Sick Leave
@ -41,7 +67,9 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""}`} className={`tab text-xs font-semibold ${
activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")} onClick={() => setActiveTab("adjustment")}
> >
Adjustment Submission Adjustment Submission
@ -51,43 +79,22 @@ const LeaveAndAdjustments: React.FC = () => {
{/* Main Container */} {/* Main Container */}
<div className="rounded-lg bg-base-100 overflow-hidden"> <div className="rounded-lg bg-base-100 overflow-hidden">
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]"> <div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
{activeTab === 'pto' && (
<div className="animate-fade-in"> {activeTab === "pto" && (
<PtoLeaveTab /> <PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
</div>
)} )}
{activeTab === 'sick' && ( {activeTab === "sick" && (
<div className="animate-fade-in"> <SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
<SickLeaveTab />
</div>
)} )}
{activeTab === 'adjustment' && ( {activeTab === "adjustment" && (
<div className="animate-fade-in"> <AdjustmentTab projects={projects} />
<AdjustmentTab projects={projects} />
</div>
)} )}
</div> </div>
</div> </div>
</div> </div>
<style>{`
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
`}</style>
</div> </div>
); );
}; };

View File

@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
interface Project { interface Project {
projectId: string; projectId: string;
projectName: string; projectName: string;
status?: string | boolean;
} }
interface ProjectSummaryResponse { interface ProjectSummaryResponse {
@ -370,7 +371,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
Pick a project Pick a project
</option> </option>
{projects.map((item, index) => ( {projects.map((item, index) => (
<option key={item.projectId || index} value={item.projectId}> <option key={item.projectId || index} value={item.projectId} className={`${item.status ? "" : ""}`}>
{item.projectName} {item.projectName}
</option> </option>
))} ))}

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react'; // PtoLeaveTab.tsx
import React, { useState } from "react";
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -6,142 +7,139 @@ interface LeaveFormData {
comment: string; comment: string;
} }
const PtoLeaveTab: React.FC = () => { const PtoLeaveTab: React.FC<{
submitLeave: Function;
isSubmitting: boolean;
}> = ({ submitLeave, isSubmitting }) => {
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({ const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
fromDate: '', fromDate: "",
toDate: '', toDate: "",
comment: '' comment: "",
}); });
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => { const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
const errors: Partial<LeaveFormData> = {};
if (!data.fromDate) { const validate = () => {
errors.fromDate = 'From date is required'; const e: Partial<LeaveFormData> = {};
const today = new Date();
today.setHours(0, 0, 0, 0);
const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null;
const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null;
// From date required
if (!from) {
e.fromDate = "From date is required";
} else if (from < today) {
e.fromDate = "From date cannot be in the past";
} }
if (!data.toDate) { // To date required
errors.toDate = 'To date is required'; if (!to) {
e.toDate = "To date is required";
} else if (to < today) {
e.toDate = "To date cannot be in the past";
} }
if (!data.comment.trim()) { // To date must be same or after From date
errors.comment = 'Comment is required'; if (from && to && to < from) {
e.toDate = "To date cannot be before From date";
} }
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) { // Comment validation
errors.toDate = 'To date must be after from date'; if (!ptoLeave.comment.trim()) {
e.comment = "Comment required";
} }
return errors; return e;
}; };
const handlePtoChange = (field: keyof LeaveFormData, value: string) => { const handleSubmit = async () => {
setPtoLeave(prev => ({ ...prev, [field]: value })); const e = validate();
// Clear error for the field being changed if (Object.keys(e).length) return setErrors(e);
if (ptoErrors[field]) {
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handlePtoSubmit = async () => { const success = await submitLeave({
const errors = validateLeaveForm(ptoLeave); dateFrom: ptoLeave.fromDate,
dateTo: ptoLeave.toDate,
leaveType: "PTO",
comment: ptoLeave.comment,
});
if (Object.keys(errors).length > 0) { if (success) {
setPtoErrors(errors); setPtoLeave({ fromDate: "", toDate: "", comment: "" });
return; setErrors({});
} alert("PTO submitted!");
try {
console.log('Submitting PTO:', ptoLeave);
alert('PTO request submitted successfully!');
setPtoLeave({ fromDate: '', toDate: '', comment: "" });
setPtoErrors({});
} catch (error) {
alert('Failed to submit request. Please try again.');
console.error('Error:', error);
} }
}; };
return ( return (
<div className="max-w-4xl"> <div className="max-w-4xl space-y-3">
<div className="space-y-3"> <div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-4">
{/* From Date */}
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-sm">From Date</span>
</label>
<input
type="date"
value={ptoLeave.fromDate}
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
ptoErrors.fromDate ? 'input-error' : ''
}`}
/>
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.fromDate}</span>
</label>
)}
</div>
{/* To Date */} {/* From Date */}
<div className="form-control"> <div className="form-control">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm">To Date</span> <span className="label-text font-semibold text-sm">From Date</span>
</label> </label>
<input <input
type="date" type="date"
value={ptoLeave.toDate} disabled={isSubmitting}
onChange={(e) => handlePtoChange('toDate', e.target.value)} value={ptoLeave.fromDate}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ min={new Date().toISOString().split("T")[0]}
ptoErrors.toDate ? 'input-error' : '' onChange={(e) => setPtoLeave({ ...ptoLeave, fromDate: e.target.value })}
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
}`} }`}
/> />
{ptoErrors.toDate && ( {errors.fromDate && <p className="text-error text-xs">{errors.fromDate}</p>}
<label className="label">
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div>
{/* Description/Comment Field */}
<div className="form-control flex flex-col gap-1 col-span-2">
<label className="label">
<span className="label-text font-semibold text-sm">Description / Reason</span>
</label>
<textarea
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
ptoErrors.comment ? 'textarea-error' : ''
}`}
placeholder="Please enter the reason for your PTO request to help us better understand your needs."
value={ptoLeave.comment}
onChange={(e) => handlePtoChange('comment', e.target.value)}
/>
{ptoErrors.comment && (
<label className="label">
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.comment}</span>
</label>
)}
</div>
</div> </div>
{/* Submit Button */} {/* To Date */}
<div className="flex gap-3"> <div className="form-control">
<button <label className="label">
onClick={handlePtoSubmit} <span className="label-text font-semibold text-sm">To Date</span>
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300" </label>
> <input
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> type="date"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> disabled={isSubmitting}
</svg> value={ptoLeave.toDate}
Submit PTO Request min={new Date().toISOString().split("T")[0]}
</button> onChange={(e) => setPtoLeave({ ...ptoLeave, toDate: e.target.value })}
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
}`}
/>
{errors.toDate && <p className="text-error text-xs">{errors.toDate}</p>}
</div>
{/* Comment */}
<div className="form-control col-span-2">
<label className="label">
<span className="label-text font-semibold text-sm">Description</span>
</label>
<textarea
disabled={isSubmitting}
value={ptoLeave.comment}
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
onChange={(e) => setPtoLeave({ ...ptoLeave, comment: e.target.value })}
className={`textarea textarea-bordered text-xs h-40 ${errors.comment ? "textarea-error" : ""
}`}
/>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div> </div>
</div> </div>
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="btn btn-primary flex-1"
>
{isSubmitting ? (
<span className="loading loading-spinner loading-sm"></span>
) : (
"Submit PTO Request"
)}
</button>
</div> </div>
); );
}; };

View File

@ -6,7 +6,10 @@ interface LeaveFormData {
comment: string; comment: string;
} }
const SickLeaveTab: React.FC = () => { const SickLeaveTab: React.FC<{
submitLeave: Function;
isSubmitting: boolean;
}> = ({ submitLeave, isSubmitting }) => {
const [sickLeave, setSickLeave] = useState<LeaveFormData>({ const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '', fromDate: '',
toDate: '', toDate: '',
@ -43,7 +46,7 @@ const SickLeaveTab: React.FC = () => {
} }
}; };
const handleSickSubmit = async () => { const handleSubmit = async () => {
const errors = validateLeaveForm(sickLeave); const errors = validateLeaveForm(sickLeave);
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
@ -51,18 +54,23 @@ const SickLeaveTab: React.FC = () => {
return; return;
} }
try { const success = await submitLeave({
console.log('Submitting Sick Leave:', sickLeave); dateFrom: sickLeave.fromDate,
alert('Sick Leave request submitted successfully!'); dateTo: sickLeave.toDate,
leaveType: "SICK",
comment: sickLeave.comment,
});
setSickLeave({ fromDate: '', toDate: '', comment: '' }); if (success) {
setSickLeave({ fromDate: "", toDate: "", comment: "" });
setSickErrors({}); setSickErrors({});
} catch (error) { alert("SICK submitted!");
alert('Failed to submit request. Please try again.');
console.error('Error:', error);
} }
}; };
return ( return (
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="space-y-3"> <div className="space-y-3">
@ -75,10 +83,11 @@ const SickLeaveTab: React.FC = () => {
<input <input
type="date" type="date"
value={sickLeave.fromDate} value={sickLeave.fromDate}
min={new Date().toISOString().split("T")[0]}
disabled={isSubmitting}
onChange={(e) => handleSickChange('fromDate', e.target.value)} onChange={(e) => handleSickChange('fromDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ className={`input input-bordered input-sm w-full focus:input-primary transition-all ${sickErrors.fromDate ? 'input-error' : ''
sickErrors.fromDate ? 'input-error' : '' }`}
}`}
/> />
{sickErrors.fromDate && ( {sickErrors.fromDate && (
<label className="label"> <label className="label">
@ -95,10 +104,11 @@ const SickLeaveTab: React.FC = () => {
<input <input
type="date" type="date"
value={sickLeave.toDate} value={sickLeave.toDate}
min={new Date().toISOString().split("T")[0]}
disabled={isSubmitting}
onChange={(e) => handleSickChange('toDate', e.target.value)} onChange={(e) => handleSickChange('toDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ className={`input input-bordered input-sm w-full focus:input-primary transition-all ${sickErrors.toDate ? 'input-error' : ''
sickErrors.toDate ? 'input-error' : '' }`}
}`}
/> />
{sickErrors.toDate && ( {sickErrors.toDate && (
<label className="label"> <label className="label">
@ -110,14 +120,14 @@ const SickLeaveTab: React.FC = () => {
{/* Description/Comment Field */} {/* Description/Comment Field */}
<div className="form-control flex flex-col gap-1 col-span-2"> <div className="form-control flex flex-col gap-1 col-span-2">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm">Description / Reason</span> <span className="label-text font-semibold text-sm">Description</span>
</label> </label>
<textarea <textarea
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 ${sickErrors.comment ? 'textarea-error' : ''
sickErrors.comment ? 'textarea-error' : '' }`}
}`}
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation." placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
value={sickLeave.comment} value={sickLeave.comment}
disabled={isSubmitting}
onChange={(e) => handleSickChange('comment', e.target.value)} onChange={(e) => handleSickChange('comment', e.target.value)}
/> />
{sickErrors.comment && ( {sickErrors.comment && (
@ -129,17 +139,17 @@ const SickLeaveTab: React.FC = () => {
</div> </div>
{/* Submit Button */} {/* Submit Button */}
<div className="flex gap-3"> <button
<button onClick={handleSubmit}
onClick={handleSickSubmit} disabled={isSubmitting}
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300" className="btn btn-primary flex-1"
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {isSubmitting ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <span className="loading loading-spinner loading-sm"></span>
</svg> ) : (
Submit Sick Leave Request "Submit Sick Request"
</button> )}
</div> </button>
</div> </div>
</div> </div>
); );

View File

@ -5,7 +5,7 @@ import { setUserSlice } from '../store/userSlice';
import DocVaLogoLight from '../../assets/logo-colored-vertical.svg' import DocVaLogoLight from '../../assets/logo-colored-vertical.svg'
import DocVaLogoDark from '../../assets/logo-white-vertical.svg' import DocVaLogoDark from '../../assets/logo-white-vertical.svg'
import { useTheme } from '../helper/ThemeToggle'; import { useTheme } from '../helper/ThemeToggle';
import { KeyRound, Mail } from 'lucide-react'; import { Eye, EyeOff, KeyRound, Mail } from 'lucide-react';
const Login = () => { const Login = () => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
@ -18,6 +18,7 @@ const Login = () => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [isForgotMode, setIsForgotMode] = useState(false); const [isForgotMode, setIsForgotMode] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
@ -116,7 +117,7 @@ const Login = () => {
<span className="label-text font-medium">Email Address</span> <span className="label-text font-medium">Email Address</span>
</label> </label>
<label className="input input-bordered flex items-center gap-2" tabIndex={0}> <label className="input input-bordered flex items-center gap-2" tabIndex={0}>
<Mail size={18} /> {/* <Mail size={18} /> */}
<input <input
type="email" type="email"
name="email" name="email"
@ -129,23 +130,41 @@ const Login = () => {
</label> </label>
</div> </div>
{!isForgotMode && <div className="form-control w-90 px-5 mt-3"> {!isForgotMode && (
<label className="label"> <div className="form-control w-90 px-5 mt-3">
<span className="label-text font-medium">Password</span> <label className="label">
</label> <span className="label-text font-medium">Password</span>
<label className="input input-bordered flex items-center gap-2" tabIndex={0}> </label>
<KeyRound size={18} />
<input <label className="input input-bordered flex items-center gap-2" tabIndex={0}>
type="password" {/* <KeyRound size={18} /> */}
name="password"
placeholder="Enter your password" <input
className="grow" type={showPassword ? "text" : "password"}
value={password} name="password"
onChange={(e) => setPassword(e.target.value)} placeholder="Enter your password"
required className="grow"
/> value={password}
</label> onChange={(e) => setPassword(e.target.value)}
</div>} required
/>
{/* Toggle Icon */}
<button
type="button"
className="cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
>
{showPassword ? (
<EyeOff size={18} className="opacity-70" />
) : (
<Eye size={18} className="opacity-70" />
)}
</button>
</label>
</div>
)}
<div className="form-control w-90 px-5 mt-4"> <div className="form-control w-90 px-5 mt-4">
<button <button

View File

@ -28,6 +28,7 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
status:string
} }
const Timesheets: React.FC = () => { const Timesheets: React.FC = () => {
@ -161,7 +162,7 @@ const Timesheets: React.FC = () => {
</thead> </thead>
<tbody> <tbody>
{projectsData.map((entry, index) => ( {projectsData.map((entry, index) => (
<tr key={entry.projectId}> <tr key={entry.projectId} className={`${entry.status ==='INACTIVE' && 'hidden'}`}>
<td>{index + 1}</td> <td>{index + 1}</td>
<td>{entry.projectName}</td> <td>{entry.projectName}</td>
<td>{formatWorkTime(entry.totalWorkTime)}</td> <td>{formatWorkTime(entry.totalWorkTime)}</td>
@ -181,10 +182,10 @@ const Timesheets: React.FC = () => {
</div> </div>
{/* Footer Summary */} {/* Footer Summary */}
<div className="flex justify-between items-center text-xs mt-2"> <div className="flex justify-end items-center text-xs mt-2">
<span className="text-base-content/60"> {/* <span className="text-base-content/60">
Showing {timesheetData.length} entries Showing {timesheetData.length} entries
</span> </span> */}
<span className="text-base-content/60"> <span className="text-base-content/60">
Last updated: {new Date().toLocaleDateString()} Last updated: {new Date().toLocaleDateString()}
</span> </span>

13
types.d.ts vendored
View File

@ -86,6 +86,12 @@ interface ProfileData {
_id: string; _id: string;
} }
type LeaveRequestResponse = {
success: boolean;
data?: any;
message?: string;
};
interface Window { interface Window {
electron: { electron: {
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
@ -163,7 +169,14 @@ interface Window {
data?: any; data?: any;
message?: string; message?: string;
}>; }>;
createPTOAndSickLeaveRequest: (
payload: LeaveRequestPayload
) => Promise<LeaveRequestResponse>;
}; };
} }