Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker into window-build
This commit is contained in:
commit
882e8349c9
|
|
@ -11,6 +11,7 @@ node_modules
|
|||
dist
|
||||
dist-react
|
||||
dist-electron
|
||||
release
|
||||
*.local
|
||||
release
|
||||
|
||||
|
|
|
|||
BIN
appIcon.png
BIN
appIcon.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 435 KiB |
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 435 KiB |
|
|
@ -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 = () => {
|
||||
console.log("⚠️ Sending token-expired event to renderer");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,28 @@
|
|||
// import path from 'path';
|
||||
// import { app } from 'electron';
|
||||
// import { isDev } from './util.js';
|
||||
|
||||
// export function getPreloadPath() {
|
||||
// return path.join(
|
||||
// app.getAppPath(),
|
||||
// isDev() ? '.' : '..',
|
||||
// '/dist-electron/preload.cjs'
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function getUIPath() {
|
||||
// return path.join(app.getAppPath(), "dist-react", "index.html");
|
||||
|
||||
// // return "C:\\Users\\user\\OneDrive\\Desktop\\electron-main\\dist-react\\index.html";
|
||||
// // return path.join(app.getAppPath(), '/dist-react/index.html');
|
||||
// }
|
||||
|
||||
// export function getAssetPath() {
|
||||
// return path.join(app.getAppPath(), isDev() ? '.' : '..', '/src/assets');
|
||||
// }
|
||||
|
||||
|
||||
|
||||
import path from 'path';
|
||||
import { app } from 'electron';
|
||||
import { isDev } from './util.js';
|
||||
|
|
@ -6,17 +31,22 @@ export function getPreloadPath() {
|
|||
return path.join(
|
||||
app.getAppPath(),
|
||||
isDev() ? '.' : '..',
|
||||
'/dist-electron/preload.cjs'
|
||||
'/dist-electron/preload.cjs' // ✅ Changed to .js
|
||||
);
|
||||
}
|
||||
|
||||
export function getUIPath() {
|
||||
return path.join(app.getAppPath(), "dist-react", "index.html");
|
||||
|
||||
// return "C:\\Users\\user\\OneDrive\\Desktop\\electron-main\\dist-react\\index.html";
|
||||
// return path.join(app.getAppPath(), '/dist-react/index.html');
|
||||
}
|
||||
|
||||
export function getAssetPath() {
|
||||
return path.join(app.getAppPath(), isDev() ? '.' : '..', '/src/assets');
|
||||
return path.join(app.getAppPath(), isDev() ? '.' : '..', '/assets');
|
||||
}
|
||||
|
||||
export function getIconPath() {
|
||||
return path.join(getAssetPath(), 'icons', 'appIcon.ico');
|
||||
}
|
||||
|
||||
export function getSplashPath() {
|
||||
return path.join(getAssetPath(), 'splash.html');
|
||||
}
|
||||
|
|
@ -50,6 +50,10 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
|
||||
createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data),
|
||||
|
||||
createPTOAndSickLeaveRequest: (data) =>
|
||||
ipcRenderer.invoke("create-leave-request", data),
|
||||
|
||||
|
||||
} satisfies Window['electron']);
|
||||
|
||||
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ export const handleLogin = async (credentials: { email: string, password: string
|
|||
console.log("UIPATH", getUIPath());
|
||||
console.log("Doom 1");
|
||||
|
||||
|
||||
try {
|
||||
console.log("➡️ Calling API...");
|
||||
const response = await axios.post(
|
||||
|
|
@ -190,4 +189,7 @@ export const captureSpecificApp = async (windowTitle: string): Promise<string> =
|
|||
console.error('Error capturing screenshot:', error);
|
||||
return 'Failed to capture screenshot.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// timmer stop after 4-5 sec check it..
|
||||
|
|
@ -107,7 +107,7 @@ const DashboardLayout = () => {
|
|||
const menuItems = [
|
||||
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
|
||||
{ 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: "VPN", path: "/dashboard/vpn", icon: <GlobeLock 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
|
||||
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);
|
||||
console.log("🧹 Active project log ID cleared");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
|
|
@ -9,23 +10,46 @@ type TabType = 'pto' | 'sick' | 'adjustment';
|
|||
|
||||
const LeaveAndAdjustments: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('pto');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false); // 🚀 NEW
|
||||
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 (
|
||||
<div className="max-h-screen">
|
||||
<div className="w-full mx-auto">
|
||||
{/* Header */}
|
||||
<div className="">
|
||||
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||
</div>
|
||||
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div role="tablist" className="tabs tabs-border w-full mb-1">
|
||||
{/* Tabs — Disabled while submitting */}
|
||||
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100">
|
||||
<a
|
||||
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")}
|
||||
>
|
||||
PTO Leave
|
||||
|
|
@ -33,7 +57,9 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
|
||||
<a
|
||||
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")}
|
||||
>
|
||||
Sick Leave
|
||||
|
|
@ -41,7 +67,9 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
|
||||
<a
|
||||
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")}
|
||||
>
|
||||
Adjustment Submission
|
||||
|
|
@ -51,45 +79,24 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
{/* Main Container */}
|
||||
<div className="rounded-lg bg-base-100 overflow-hidden">
|
||||
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
|
||||
{activeTab === 'pto' && (
|
||||
<div className="animate-fade-in">
|
||||
<PtoLeaveTab />
|
||||
</div>
|
||||
|
||||
{activeTab === "pto" && (
|
||||
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
|
||||
)}
|
||||
|
||||
{activeTab === 'sick' && (
|
||||
<div className="animate-fade-in">
|
||||
<SickLeaveTab />
|
||||
</div>
|
||||
|
||||
{activeTab === "sick" && (
|
||||
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
|
||||
)}
|
||||
|
||||
{activeTab === 'adjustment' && (
|
||||
<div className="animate-fade-in">
|
||||
<AdjustmentTab projects={projects} />
|
||||
</div>
|
||||
|
||||
{activeTab === "adjustment" && (
|
||||
<AdjustmentTab projects={projects} />
|
||||
)}
|
||||
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveAndAdjustments;
|
||||
export default LeaveAndAdjustments;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
|
|||
interface Project {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
status?: string | boolean;
|
||||
}
|
||||
|
||||
interface ProjectSummaryResponse {
|
||||
|
|
@ -370,7 +371,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
|||
Pick a project
|
||||
</option>
|
||||
{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}
|
||||
</option>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
// PtoLeaveTab.tsx
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
|
|
@ -6,144 +7,141 @@ interface LeaveFormData {
|
|||
comment: string;
|
||||
}
|
||||
|
||||
const PtoLeaveTab: React.FC = () => {
|
||||
const PtoLeaveTab: React.FC<{
|
||||
submitLeave: Function;
|
||||
isSubmitting: boolean;
|
||||
}> = ({ submitLeave, isSubmitting }) => {
|
||||
|
||||
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
comment: ''
|
||||
fromDate: "",
|
||||
toDate: "",
|
||||
comment: "",
|
||||
});
|
||||
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||
const errors: Partial<LeaveFormData> = {};
|
||||
const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
if (!data.fromDate) {
|
||||
errors.fromDate = 'From date is required';
|
||||
const validate = () => {
|
||||
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) {
|
||||
errors.toDate = 'To date is required';
|
||||
// To date 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()) {
|
||||
errors.comment = 'Comment is required';
|
||||
// To date must be same or after From date
|
||||
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)) {
|
||||
errors.toDate = 'To date must be after from date';
|
||||
// Comment validation
|
||||
if (!ptoLeave.comment.trim()) {
|
||||
e.comment = "Comment required";
|
||||
}
|
||||
|
||||
return errors;
|
||||
return e;
|
||||
};
|
||||
|
||||
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
|
||||
setPtoLeave(prev => ({ ...prev, [field]: value }));
|
||||
// Clear error for the field being changed
|
||||
if (ptoErrors[field]) {
|
||||
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
const e = validate();
|
||||
if (Object.keys(e).length) return setErrors(e);
|
||||
|
||||
const handlePtoSubmit = async () => {
|
||||
const errors = validateLeaveForm(ptoLeave);
|
||||
const success = await submitLeave({
|
||||
dateFrom: ptoLeave.fromDate,
|
||||
dateTo: ptoLeave.toDate,
|
||||
leaveType: "PTO",
|
||||
comment: ptoLeave.comment,
|
||||
});
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setPtoErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (success) {
|
||||
setPtoLeave({ fromDate: "", toDate: "", comment: "" });
|
||||
setErrors({});
|
||||
alert("PTO submitted!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
<div className="max-w-4xl space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
{/* To Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">To Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={ptoLeave.toDate}
|
||||
onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
ptoErrors.toDate ? 'input-error' : ''
|
||||
{/* From Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">From Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
disabled={isSubmitting}
|
||||
value={ptoLeave.fromDate}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
onChange={(e) => setPtoLeave({ ...ptoLeave, fromDate: e.target.value })}
|
||||
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
|
||||
}`}
|
||||
/>
|
||||
{ptoErrors.toDate && (
|
||||
<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>
|
||||
/>
|
||||
{errors.fromDate && <p className="text-error text-xs">{errors.fromDate}</p>}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handlePtoSubmit}
|
||||
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Submit PTO Request
|
||||
</button>
|
||||
{/* To Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">To Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
disabled={isSubmitting}
|
||||
value={ptoLeave.toDate}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default PtoLeaveTab;
|
||||
export default PtoLeaveTab;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ interface LeaveFormData {
|
|||
comment: string;
|
||||
}
|
||||
|
||||
const SickLeaveTab: React.FC = () => {
|
||||
const SickLeaveTab: React.FC<{
|
||||
submitLeave: Function;
|
||||
isSubmitting: boolean;
|
||||
}> = ({ submitLeave, isSubmitting }) => {
|
||||
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
|
|
@ -43,7 +46,7 @@ const SickLeaveTab: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSickSubmit = async () => {
|
||||
const handleSubmit = async () => {
|
||||
const errors = validateLeaveForm(sickLeave);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
|
|
@ -51,18 +54,23 @@ const SickLeaveTab: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Submitting Sick Leave:', sickLeave);
|
||||
alert('Sick Leave request submitted successfully!');
|
||||
const success = await submitLeave({
|
||||
dateFrom: sickLeave.fromDate,
|
||||
dateTo: sickLeave.toDate,
|
||||
leaveType: "SICK",
|
||||
comment: sickLeave.comment,
|
||||
});
|
||||
|
||||
setSickLeave({ fromDate: '', toDate: '', comment: '' });
|
||||
if (success) {
|
||||
setSickLeave({ fromDate: "", toDate: "", comment: "" });
|
||||
setSickErrors({});
|
||||
} catch (error) {
|
||||
alert('Failed to submit request. Please try again.');
|
||||
console.error('Error:', error);
|
||||
alert("SICK submitted!");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="space-y-3">
|
||||
|
|
@ -75,10 +83,11 @@ const SickLeaveTab: React.FC = () => {
|
|||
<input
|
||||
type="date"
|
||||
value={sickLeave.fromDate}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
disabled={isSubmitting}
|
||||
onChange={(e) => handleSickChange('fromDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
sickErrors.fromDate ? 'input-error' : ''
|
||||
}`}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${sickErrors.fromDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{sickErrors.fromDate && (
|
||||
<label className="label">
|
||||
|
|
@ -95,10 +104,11 @@ const SickLeaveTab: React.FC = () => {
|
|||
<input
|
||||
type="date"
|
||||
value={sickLeave.toDate}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
disabled={isSubmitting}
|
||||
onChange={(e) => handleSickChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
sickErrors.toDate ? 'input-error' : ''
|
||||
}`}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${sickErrors.toDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{sickErrors.toDate && (
|
||||
<label className="label">
|
||||
|
|
@ -110,14 +120,14 @@ const SickLeaveTab: React.FC = () => {
|
|||
{/* 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>
|
||||
<span className="label-text font-semibold text-sm">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
|
||||
sickErrors.comment ? 'textarea-error' : ''
|
||||
}`}
|
||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${sickErrors.comment ? 'textarea-error' : ''
|
||||
}`}
|
||||
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
|
||||
value={sickLeave.comment}
|
||||
disabled={isSubmitting}
|
||||
onChange={(e) => handleSickChange('comment', e.target.value)}
|
||||
/>
|
||||
{sickErrors.comment && (
|
||||
|
|
@ -129,17 +139,17 @@ const SickLeaveTab: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleSickSubmit}
|
||||
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Submit Sick Leave Request
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className="btn btn-primary flex-1"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
) : (
|
||||
"Submit Sick Request"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { setUserSlice } from '../store/userSlice';
|
|||
import DocVaLogoLight from '../../assets/logo-colored-vertical.svg'
|
||||
import DocVaLogoDark from '../../assets/logo-white-vertical.svg'
|
||||
import { useTheme } from '../helper/ThemeToggle';
|
||||
import { KeyRound, Mail } from 'lucide-react';
|
||||
import { Eye, EyeOff, KeyRound, Mail } from 'lucide-react';
|
||||
|
||||
const Login = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
|
@ -18,6 +18,7 @@ const Login = () => {
|
|||
const [password, setPassword] = useState("");
|
||||
const [isForgotMode, setIsForgotMode] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
|
|
@ -116,7 +117,7 @@ const Login = () => {
|
|||
<span className="label-text font-medium">Email Address</span>
|
||||
</label>
|
||||
<label className="input input-bordered flex items-center gap-2" tabIndex={0}>
|
||||
<Mail size={18} />
|
||||
{/* <Mail size={18} /> */}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
|
|
@ -129,23 +130,41 @@ const Login = () => {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{!isForgotMode && <div className="form-control w-90 px-5 mt-3">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Password</span>
|
||||
</label>
|
||||
<label className="input input-bordered flex items-center gap-2" tabIndex={0}>
|
||||
<KeyRound size={18} />
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
className="grow"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>}
|
||||
{!isForgotMode && (
|
||||
<div className="form-control w-90 px-5 mt-3">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Password</span>
|
||||
</label>
|
||||
|
||||
<label className="input input-bordered flex items-center gap-2" tabIndex={0}>
|
||||
{/* <KeyRound size={18} /> */}
|
||||
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
className="grow"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ interface Project {
|
|||
totalWorkTime: string;
|
||||
totalWorkMs: number;
|
||||
totalLogs: number;
|
||||
status:string
|
||||
}
|
||||
|
||||
const Timesheets: React.FC = () => {
|
||||
|
|
@ -161,7 +162,7 @@ const Timesheets: React.FC = () => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{projectsData.map((entry, index) => (
|
||||
<tr key={entry.projectId}>
|
||||
<tr key={entry.projectId} className={`${entry.status ==='INACTIVE' && 'hidden'}`}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{entry.projectName}</td>
|
||||
<td>{formatWorkTime(entry.totalWorkTime)}</td>
|
||||
|
|
@ -181,10 +182,10 @@ const Timesheets: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Footer Summary */}
|
||||
<div className="flex justify-between items-center text-xs mt-2">
|
||||
<span className="text-base-content/60">
|
||||
<div className="flex justify-end items-center text-xs mt-2">
|
||||
{/* <span className="text-base-content/60">
|
||||
Showing {timesheetData.length} entries
|
||||
</span>
|
||||
</span> */}
|
||||
<span className="text-base-content/60">
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import ReactQuill from "react-quill-new";
|
||||
import Quill from "quill";
|
||||
// import ImageResize from "quill-image-resize-module-react";
|
||||
|
||||
// Quill.register("modules/imageResize", ImageResize);
|
||||
|
||||
export const quillModules = {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["bold", "italic", "underline"],
|
||||
["image", "link"],
|
||||
],
|
||||
// imageResize: {},
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// utils/timeDisplay.js
|
||||
export const getDisplayTime = (project:any, trackingData:any) => {
|
||||
if (!project) return "0h 0m 0s";
|
||||
|
||||
// If tracking is active, calculate current time based on server time + session time
|
||||
if (trackingData?.isTracking && trackingData.startTime) {
|
||||
const baseSeconds = parseTimeStringToSeconds(project.totalWorkTime);
|
||||
const sessionSeconds = Math.floor((Date.now() - trackingData.startTime) / 1000);
|
||||
const totalSeconds = baseSeconds + sessionSeconds;
|
||||
return formatSecondsToTimeString(totalSeconds);
|
||||
}
|
||||
|
||||
// If not tracking, just return the server's totalWorkTime
|
||||
return project.totalWorkTime;
|
||||
};
|
||||
|
||||
export const parseTimeStringToSeconds = (timeString:any) => {
|
||||
if (!timeString || typeof timeString !== 'string') return 0;
|
||||
|
||||
try {
|
||||
const hourMatch = timeString.match(/(\d+)h/);
|
||||
const minMatch = timeString.match(/(\d+)m/);
|
||||
const secMatch = timeString.match(/(\d+)s/);
|
||||
|
||||
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
|
||||
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
|
||||
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
} catch (error) {
|
||||
console.error('Error parsing time string:', timeString, error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatSecondsToTimeString = (totalSeconds:any) => {
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// utils/timeParser.js
|
||||
export const parseTimeStringToMs = (timeString:string) => {
|
||||
if (!timeString) return 0;
|
||||
|
||||
const hourMatch = timeString.match(/(\d+)h/);
|
||||
const minMatch = timeString.match(/(\d+)m/);
|
||||
const secMatch = timeString.match(/(\d+)s/);
|
||||
|
||||
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
|
||||
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
|
||||
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
|
||||
|
||||
return (hours * 3600 + minutes * 60 + seconds) * 1000;
|
||||
};
|
||||
|
||||
export const parseTimeStringToSeconds = (timeString:string) => {
|
||||
return Math.floor(parseTimeStringToMs(timeString) / 1000);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -86,6 +86,12 @@ interface ProfileData {
|
|||
_id: string;
|
||||
}
|
||||
|
||||
type LeaveRequestResponse = {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
electron: {
|
||||
login: (credentials: { email: string; password: string }) => Promise<any>;
|
||||
|
|
@ -163,7 +169,14 @@ interface Window {
|
|||
data?: any;
|
||||
message?: string;
|
||||
}>;
|
||||
|
||||
createPTOAndSickLeaveRequest: (
|
||||
payload: LeaveRequestPayload
|
||||
) => Promise<LeaveRequestResponse>;
|
||||
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue