This commit is contained in:
apple 2025-11-27 19:40:22 +05:30
commit ac3ddf4b2b
15 changed files with 886 additions and 325 deletions

View File

@ -30,8 +30,10 @@
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"os-utils": "^0.0.14", "os-utils": "^0.0.14",
"quill": "^2.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
"recharts": "^2.12.7", "recharts": "^2.12.7",

View File

@ -12,7 +12,7 @@ const httpsAgent = new https.Agent({
}); });
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // 🔁 your backend baseURL: 'http://localhost:3000/api/v1/va', // 🔁 your backend
httpsAgent httpsAgent
// timeout: 10000, // timeout: 10000,
}); });

View File

@ -346,22 +346,40 @@ ipcMain.handle("create-leave-request", async (event, payload) => {
const response = await api.post("/leave/request", payload); const response = await api.post("/leave/request", payload);
console.log(" Leave Saved:", response.data); console.log("Leave Saved:", response);
return { success: true, data: response.data }; return {
success: true,
data: response.data
};
} catch (error) { } catch (error: any) {
console.error("❌ Leave Save Failed:", error); console.error("❌ Leave Save Failed:");
let message = "Unknown error";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
// Extract the backend message properly
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return { return {
success: false, success: false,
error: error || "Unknown error" message
}; };
} }
}); });
export const sendTokenExpired = () => { export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer"); console.log("⚠️ Sending token-expired event to renderer");

View File

@ -68,7 +68,7 @@ export const handleLogin = async (credentials: { email: string, password: string
try { try {
console.log("➡️ Calling API..."); console.log("➡️ Calling API...");
const response = await axios.post( const response = await axios.post(
'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', 'http://localhost:3000/api/v1/va/login',
{ userDetails: { ...credentials } }, { userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
); );

View File

@ -94,6 +94,7 @@ 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";
import Vpn from "./pages/Vpn/Vpn"; import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule";
const App: React.FC = () => { const App: React.FC = () => {
@ -140,6 +141,7 @@ const App: React.FC = () => {
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} /> <Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} /> <Route path="help-support" element={<HelpSupport />} />
<Route path="schedule" element={<Schedule/>} />
</Route> </Route>
{/* Default Redirect */} {/* Default Redirect */}

View File

@ -207,6 +207,8 @@ export const useActiveAppTracker = (
const details = (await window.electron.getActiveAppDetails()) as WindowInfo; const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return; if (!details || !details.id) return;
console.log("details" , details)
const title = details.title; const title = details.title;
const appName = details.owner.name; const appName = details.owner.name;
const prevTitle = lastTitleRef.current; const prevTitle = lastTitleRef.current;

View File

@ -26,16 +26,18 @@ import {
Play, Play,
ChevronUp, ChevronUp,
GlobeLock, GlobeLock,
ClipboardClock,
} from "lucide-react"; } from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; 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 { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime } from "../store/projectSlice"; import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime, updateProjectTracking } from "../store/projectSlice";
import { clearUser } from "../store/userSlice"; import { clearUser } from "../store/userSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice";
import { canStartTracking } from "../utils/projectLimits"; import { canStartTracking } from "../utils/projectLimits";
import { useTheme } from "../helper/ThemeToggle"; import { useTheme } from "../helper/ThemeToggle";
import LogoutModal from "../modal/LogoutModal"; import LogoutModal from "../modal/LogoutModal";
import { parseTimeStringToSeconds } from "../utils/timeParser";
interface User { interface User {
id: string; id: string;
@ -77,7 +79,8 @@ export interface TrackingState {
handleProjectSelect: (projectId: string) => void; handleProjectSelect: (projectId: string) => void;
projectsData: Project[]; projectsData: Project[];
loadingStart: boolean, loadingStart: boolean,
loadingStop: boolean loadingStop: boolean,
isStopping: boolean
} }
const DashboardLayout = () => { const DashboardLayout = () => {
@ -91,6 +94,7 @@ const DashboardLayout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isStopping, setIsStopping] = useState(false);
const { projectsData, projectSummary, selectedProject, projectTrackingMap } = const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
useSelector((state: any) => state.projects); useSelector((state: any) => state.projects);
@ -108,6 +112,7 @@ const DashboardLayout = () => {
{ 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: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> }, { label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock 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} /> },
@ -123,6 +128,7 @@ const DashboardLayout = () => {
const [loadingStop, setLoadingStop] = useState(false); const [loadingStop, setLoadingStop] = useState(false);
const [showLogoutModal, setShowLogoutModal] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false);
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
}, [theme]); }, [theme]);
@ -140,16 +146,24 @@ const DashboardLayout = () => {
return; return;
} }
let currentStopTime = new Date().toISOString();
const projectId = selectedProject.projectId; const projectId = selectedProject.projectId;
console.log(" Stop tracking initiated for:", selectedProject.projectName); console.log(" Stop tracking initiated for:", selectedProject.projectName);
console.log(" Current elapsed time:", elapsedTime); console.log(" Current elapsed time:", elapsedTime);
setIsStopping(true);
setLoadingStop(true); setLoadingStop(true);
handleSocketDisconnect(); handleSocketDisconnect();
// STEP 1: Send final PATCH with last active window data // STEP 1: Clear timer FIRST to prevent further client-side updates
// This ensures the last window activity is recorded before logout if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
console.log("Timer cleared");
}
// STEP 2: Send final PATCH with last active window data
if (stopTrackingRef.current) { if (stopTrackingRef.current) {
try { try {
console.log(" Sending final PATCH to record last activity..."); console.log(" Sending final PATCH to record last activity...");
@ -157,19 +171,7 @@ const DashboardLayout = () => {
console.log(" Final tracking data sent successfully"); console.log(" Final tracking data sent successfully");
} catch (error) { } catch (error) {
console.error(" Error sending final tracking data:", error); console.error(" Error sending final tracking data:", error);
// Continue with stop even if PATCH fails
} }
} else {
console.warn("⚠️ No finalizeTracking function available");
}
// STEP 2: Stop timer everywhere
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
console.log("Timer cleared");
} else {
console.log("No active timer to clear");
} }
// STEP 3: Send LOGOUT to backend // STEP 3: Send LOGOUT to backend
@ -178,6 +180,7 @@ const DashboardLayout = () => {
vaId: projectSummary.vaId, vaId: projectSummary.vaId,
projectId: projectId, projectId: projectId,
type: "LOGOUT", type: "LOGOUT",
time: currentStopTime
}; };
console.log("📡 Sending LOGOUT request:", payload); console.log("📡 Sending LOGOUT request:", payload);
@ -187,17 +190,7 @@ const DashboardLayout = () => {
console.error(" Failed to send LOGOUT:", error); console.error(" Failed to send LOGOUT:", error);
} }
// STEP 4: Update UI (window restore) // STEP 4: Update Redux tracking state to stop tracking
try {
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
console.log(" Window UI restored");
} catch (error) {
console.error(" Error restoring window:", error);
}
// STEP 5: Update Redux tracking state → THIS TRIGGERS UI UPDATE
console.log("🔄 Updating Redux state to stop tracking..."); console.log("🔄 Updating Redux state to stop tracking...");
dispatch( dispatch(
stopTrackingReducer({ stopTrackingReducer({
@ -207,70 +200,82 @@ const DashboardLayout = () => {
// Allow React to process the state update // Allow React to process the state update
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise(resolve => setTimeout(resolve, 50));
console.log(" Redux state updated - isTracking is now false");
// STEP 6: Clear active project log ID // STEP 5: Refresh project list to get updated work hours from server
setActiveProjectLogId("");
// ✨ NEW STEP 7: Refresh project list to get updated work hours
try { try {
console.log("🔄 Refreshing project list..."); console.log("🔄 Refreshing project list...");
const res = await window.electron.getProjectList(); const projectListRes = await window.electron.getProjectList();
console.log("📊 Updated project list:", res); console.log("📊 Updated project list:", projectListRes);
if (res?.success && Array.isArray(res.data)) { if (projectListRes?.success && Array.isArray(projectListRes.data)) {
// Update projects data // Update projects data
dispatch(setProjectsDataSlice(res.data)); dispatch(setProjectsDataSlice(projectListRes.data));
// Update project summary // Update project summary
dispatch( dispatch(
setProjectSummarySlice({ setProjectSummarySlice({
totalProjects: res.totalProjects || 0, totalProjects: projectListRes.totalProjects || 0,
activeProjects: res.activeProjects || 0, activeProjects: projectListRes.activeProjects || 0,
awayProjects: res.awayProjects || 0, awayProjects: projectListRes.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s", totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0, totalWorkMs: projectListRes.totalWorkMs || 0,
vaName: res.vaName || "", vaName: projectListRes.vaName || "",
vaId: res.vaId || "", vaId: projectListRes.vaId || "",
}) })
); );
// Update selected project with fresh data // Update selected project with fresh data
const updatedProject = res.data.find( const updatedProject = projectListRes.data.find(
(p: Project) => p.projectId === projectId (p: Project) => p.projectId === projectId
); );
if (updatedProject) { if (updatedProject) {
dispatch(setSelectedProjectSlice(updatedProject)); dispatch(setSelectedProjectSlice(updatedProject));
// Update tracking map with new total work time // CRITICAL FIX: Use server time instead of client elapsedTime
const initialElapsed = Math.floor((updatedProject.totalWorkMs || 0) / 1000); const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime);
console.log("🔄 Syncing client time with server time:", {
serverTime: updatedProject.totalWorkTime,
serverElapsedSeconds: serverElapsedTime,
previousClientTime: elapsedTime,
difference: serverElapsedTime - elapsedTime
});
// Force update the tracking map with server time
dispatch( dispatch(
initializeProjectTracking({ updateProjectTracking({
projectId: updatedProject.projectId, projectId: updatedProject.projectId,
initialTime: initialElapsed, data: {
elapsedTime: serverElapsedTime,
accumulatedTime: serverElapsedTime,
isTracking: false,
isPaused: false,
startTime: null,
endTime: Date.now(),
pauseStartTime: null,
totalPausedTime: 0
}
}) })
); );
console.log("✅ Selected project updated with fresh data"); console.log("✅ Selected project updated with server time");
} }
} }
// STEP 6: Update UI (window restore)
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
} catch (error) { } catch (error) {
console.error("❌ Error refreshing project list:", error); console.error("❌ Error refreshing project list:", error);
} } finally {
// Clear loading states
setLoadingStop(false); setLoadingStop(false);
console.log("🧹 Active project log ID cleared"); setIsStopping(false);
console.log("=".repeat(50));
console.log("🎉 TRACKING STOPPED SUCCESSFULLY"); console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
console.log(`Project: ${selectedProject.projectName}`); }
console.log(`Final Time: ${formatTime(elapsedTime)}`);
console.log("UI Updates:");
console.log(" ✓ Header should be hidden");
console.log(" ✓ Start button should be visible");
console.log(" ✓ Layout should expand to full height");
console.log("=".repeat(50));
}, [selectedProject, projectSummary, elapsedTime, dispatch]); }, [selectedProject, projectSummary, elapsedTime, dispatch]);
// NOW define handleLimitExceeded using the defined handleStopTracking // NOW define handleLimitExceeded using the defined handleStopTracking
@ -361,7 +366,6 @@ const DashboardLayout = () => {
: null; : null;
const handleStartTracking = async (project: Project) => { const handleStartTracking = async (project: Project) => {
if (loadingStart || loadingStop) return; if (loadingStart || loadingStop) return;
// Check limit before starting // Check limit before starting
@ -381,13 +385,14 @@ const DashboardLayout = () => {
setLoadingStart(true); setLoadingStart(true);
const now = Date.now(); const now = Date.now();
let currentStartTime = new Date().toISOString();
window.electron.toggleCloseBtn(false); window.electron.toggleCloseBtn(false);
const payload = { const payload = {
vaId: projectSummary.vaId, vaId: projectSummary.vaId,
projectId: project.projectId, projectId: project.projectId,
type: "LOGIN", type: "LOGIN",
time: currentStartTime
}; };
try { try {
@ -399,6 +404,15 @@ const DashboardLayout = () => {
return; return;
} }
// CRITICAL FIX: Get the current accumulated time BEFORE starting timer
const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime);
console.log("📊 Starting with accumulated time:", {
accumulatedTime: currentAccumulatedTime,
projectTotalWorkTime: project.totalWorkTime,
parsedTime: parseTimeStringToSeconds(project.totalWorkTime)
});
// ⬇️ REDUX UPDATE: Start Tracking // ⬇️ REDUX UPDATE: Start Tracking
dispatch( dispatch(
startTracking({ startTracking({
@ -407,22 +421,17 @@ const DashboardLayout = () => {
}) })
); );
handleSocketConnect() handleSocketConnect();
setActiveProjectLogId(res?.data?.logId); setActiveProjectLogId(res?.data?.logId);
console.log(" Tracking started. Log ID:", res?.data?.logId); console.log(" Tracking started. Log ID:", res?.data?.logId);
// ⬇️ Calculate time limit in seconds // ⬇️ Calculate time limit in seconds
const parseTimeToSeconds = (timeStr: string): number => { const parseTimeToSeconds = (timeStr: string): number => {
const hourMatch = timeStr.match(/(\d+)h/); return parseTimeStringToSeconds(timeStr);
const minMatch = timeStr.match(/(\d+)m/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return (hours * 3600) + (minutes * 60);
}; };
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours); const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
const alreadyWorkedSeconds = Math.floor((project.totalWorkMs || 0) / 1000); const alreadyWorkedSeconds = parseTimeStringToSeconds(project.totalWorkTime);
const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds; const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds;
console.log("⏱️ Time Limits:", { console.log("⏱️ Time Limits:", {
@ -433,10 +442,19 @@ const DashboardLayout = () => {
remainingMinutes: Math.floor(remainingSeconds / 60) remainingMinutes: Math.floor(remainingSeconds / 60)
}); });
// ⬇️ Start global timer loop with limit check // ⬇️ FIXED: Start timer with proper initial time
// Set initial elapsed time immediately to avoid jump
dispatch(
updateElapsedTime({
projectId: project.projectId,
elapsedTime: currentAccumulatedTime,
})
);
// Start global timer loop with limit check
timerRef.current = setInterval(() => { timerRef.current = setInterval(() => {
const currentElapsed = Math.floor((Date.now() - now) / 1000) + const timeSinceStart = Math.floor((Date.now() - now) / 1000);
(selectedData?.accumulatedTime || 0); const currentElapsed = currentAccumulatedTime + timeSinceStart;
dispatch( dispatch(
updateElapsedTime({ updateElapsedTime({
@ -446,7 +464,7 @@ const DashboardLayout = () => {
); );
// Check if time limit reached // Check if time limit reached
const totalTimeWorked = alreadyWorkedSeconds + Math.floor((Date.now() - now) / 1000); const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart;
if (totalTimeWorked >= dedicatedSeconds) { if (totalTimeWorked >= dedicatedSeconds) {
console.warn("🚨 CLIENT-SIDE: Time limit reached!"); console.warn("🚨 CLIENT-SIDE: Time limit reached!");
@ -496,9 +514,11 @@ const DashboardLayout = () => {
const existing = projectTrackingMap[projectId]; const existing = projectTrackingMap[projectId];
if (!existing) { if (!existing) {
// Use parsed totalWorkTime instead of totalWorkMs
const initialTime = parseTimeStringToSeconds(project.totalWorkTime);
dispatch(initializeProjectTracking({ dispatch(initializeProjectTracking({
projectId, projectId,
initialTime: Math.floor((project.totalWorkMs || 0) / 1000) initialTime: initialTime
})); }));
} }
}; };
@ -594,7 +614,8 @@ const DashboardLayout = () => {
projectsData, projectsData,
handleProjectSelect, handleProjectSelect,
loadingStart, loadingStart,
loadingStop loadingStop,
isStopping
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@ -611,7 +632,13 @@ const DashboardLayout = () => {
{isTracking && {isTracking &&
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30"> <header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{selectedProject ? ( {isStopping ? (
// Skeleton for left side during stop
<div className="flex items-center space-x-3">
<div className="skeleton w-8 h-8 rounded-full"></div>
<div className="skeleton w-32 h-4"></div>
</div>
) : selectedProject ? (
isTracking ? ( isTracking ? (
<button <button
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
@ -625,7 +652,6 @@ const DashboardLayout = () => {
<Pause size={16} /> <Pause size={16} />
)} )}
</button> </button>
) : ( ) : (
<button <button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
@ -645,20 +671,31 @@ const DashboardLayout = () => {
</button> </button>
)} )}
{selectedProject ? ( {!isStopping && selectedProject ? (
<span className="ml-2 text-sm text-base-content/70"> <span className="ml-2 text-sm text-base-content/70">
{selectedProject.projectName} {selectedProject.projectName}
</span> </span>
) : ( ) : !isStopping ? (
<span className="ml-2 text-sm text-base-content/40 italic"> <span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected No Project Selected
</span> </span>
)} ) : null}
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{isStopping ? (
// Skeleton for right side during stop
<div className="flex items-center space-x-3">
<div className="skeleton w-20 h-6 rounded"></div>
<div className="skeleton w-6 h-6 rounded"></div>
<div className="skeleton w-8 h-8 rounded"></div>
</div>
) : (
<>
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm"> <div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(elapsedTime || 0)} {formatTime(
isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s")
)}
</div> </div>
<span className="text-error"> <span className="text-error">
@ -679,6 +716,8 @@ const DashboardLayout = () => {
> >
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />} {toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
</button> </button>
</>
)}
</div> </div>
</header> </header>
} }

View File

@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice"; import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice";
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits'; import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
import { parseTimeStringToSeconds } from "../utils/timeParser";
// Updated Project interface to match JSON structure // Updated Project interface to match JSON structure
interface Project { interface Project {
@ -40,7 +41,8 @@ interface TrackingState {
handleProjectSelect: (projectId: string) => void; handleProjectSelect: (projectId: string) => void;
projectsData: Project[]; projectsData: Project[];
loadingStart: boolean, loadingStart: boolean,
loadingStop: boolean loadingStop: boolean,
isStopping: boolean
} }
// Helper function // Helper function
@ -86,8 +88,6 @@ const Widget: React.FC<{
// Dashboard Component // Dashboard Component
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const { const {
// selectedProject,
// projectTrackingMap,
isTracking, isTracking,
isPaused, isPaused,
elapsedTime, elapsedTime,
@ -96,31 +96,20 @@ const Dashboard: React.FC = () => {
handlePauseResume, handlePauseResume,
handleStopTracking, handleStopTracking,
handleProjectSelect, handleProjectSelect,
// projectsData,
loadingStart, loadingStart,
loadingStop loadingStop,
isStopping
} = useOutletContext<TrackingState>(); } = useOutletContext<TrackingState>();
// interface AppsListInt {
// name: string;
// pid: string;
// title: string;
// }
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
const projectsData = useSelector((state: any) => state.projects.projectsData); const projectsData = useSelector((state: any) => state.projects.projectsData);
const selectedProject = useSelector((state: any) => state.projects.selectedProject); const selectedProject = useSelector((state: any) => state.projects.selectedProject);
const projectSummary = useSelector((state: any) => state.projects.projectSummary); const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
console.log("Line 111", projectsData, selectedProject, projectTrackingMap) console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
const dispatch = useDispatch(); const dispatch = useDispatch();
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
dispatch(setLoading(true)); dispatch(setLoading(true));
@ -146,20 +135,6 @@ const Dashboard: React.FC = () => {
console.log(" Project list response in dashboard:", res); console.log(" Project list response in dashboard:", res);
if (res?.success && Array.isArray(res.data)) { if (res?.success && Array.isArray(res.data)) {
// Store project list
// setProjectsData(res.data);
// Store project summary info
// setProjectSummary({
// 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 || "",
// });
// Store data redux store // Store data redux store
dispatch(setProjectsDataSlice(res.data)); dispatch(setProjectsDataSlice(res.data));
@ -182,10 +157,9 @@ const Dashboard: React.FC = () => {
// Select the project // Select the project
dispatch(setSelectedProjectSlice(firstProject)); dispatch(setSelectedProjectSlice(firstProject));
// Convert total work ms → seconds const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime);
const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000);
// Initialize tracking data for first project (your slice matches this) // Initialize tracking data for first project
dispatch( dispatch(
initializeProjectTracking({ initializeProjectTracking({
projectId: firstProject.projectId, projectId: firstProject.projectId,
@ -198,9 +172,6 @@ const Dashboard: React.FC = () => {
} else { } else {
console.warn("⚠️ Unexpected response:", res); console.warn("⚠️ Unexpected response:", res);
// if (res?.status) {
// // handleLogout()
// }
} }
} catch (err) { } catch (err) {
console.error("❌ Error fetching projects:", err); console.error("❌ Error fetching projects:", err);
@ -214,6 +185,20 @@ const Dashboard: React.FC = () => {
? projectTrackingMap[selectedProject.projectId] ? projectTrackingMap[selectedProject.projectId]
: null; : null;
// FIX: Get the correct time to display
const getDisplayTime = () => {
if (!selectedProject) return 0;
// If tracking is active, use real-time elapsedTime
if (selectedData?.isTracking) {
return selectedData.elapsedTime || 0;
}
// If not tracking, use server time from selectedProject
return parseTimeStringToSeconds(selectedProject.totalWorkTime);
};
const displayTime = getDisplayTime();
const formatWorkTime = (time: string) => { const formatWorkTime = (time: string) => {
if (!time) return "0h 0m"; if (!time) return "0h 0m";
@ -225,9 +210,7 @@ const Dashboard: React.FC = () => {
return parts.slice(0, 2).join(" "); return parts.slice(0, 2).join(" ");
}; };
const handleProjectSelectLocal = (projectId: string) => { const handleProjectSelectLocal = (projectId: string) => {
const project = projectsData.find((p: any) => p.projectId === projectId); const project = projectsData.find((p: any) => p.projectId === projectId);
if (!project) return; if (!project) return;
@ -236,8 +219,7 @@ const Dashboard: React.FC = () => {
const existingData = projectTrackingMap[projectId]; const existingData = projectTrackingMap[projectId];
if (!existingData) { if (!existingData) {
const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000); const initialElapsed = parseTimeStringToSeconds(project.totalWorkTime);
dispatch(initializeProjectTracking({ dispatch(initializeProjectTracking({
projectId, projectId,
initialTime: initialElapsed initialTime: initialElapsed
@ -251,7 +233,6 @@ const Dashboard: React.FC = () => {
} }
}; };
return ( return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen"> <main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
{/* Header */} {/* Header */}
@ -262,23 +243,35 @@ const Dashboard: React.FC = () => {
<div className="w-full mb-4"> <div className="w-full mb-4">
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm"> <div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body"> <div className="card-body">
{selectedProject ? ( {isStopping ? (
// Skeleton loader for tracking section
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-center sm:text-left w-full">
<div className="skeleton w-32 h-6 mx-auto sm:mx-0 mb-2"></div>
<div className="skeleton w-48 h-4 mx-auto sm:mx-0"></div>
</div>
<div className="skeleton w-full sm:w-32 h-10 rounded-xl"></div>
</div>
) : selectedProject ? (
<> <>
<div className="flex flex-col sm:flex-row items-center justify-between gap-3"> <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">
{/* FIX: Use displayTime instead of always using server time */}
<p className="text-lg font-extrabold text-success"> <p className="text-lg font-extrabold text-success">
{formatTime(selectedData?.elapsedTime || 0)} {formatTime(displayTime)}
</p> </p>
{/* <p className="text-xs text-base-content/70 mt-1">
{selectedData?.isTracking ? "Current Session" : "Ready"}
</p> */}
<div> <div>
<span className="text-base-content/70">Project:</span>{" "} <span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold"> <span className="font-semibold">
{selectedProject.projectName} {selectedProject.projectName}
</span> </span>
</div> </div>
{/* Show tracking status */}
{selectedData?.isTracking && (
<div className="text-xs text-success mt-1">
● Live Tracking
</div>
)}
</div> </div>
{!selectedData?.isTracking ? ( {!selectedData?.isTracking ? (
@ -308,9 +301,7 @@ const Dashboard: React.FC = () => {
{loadingStop ? "Stopping..." : "Stop Tracking"} {loadingStop ? "Stopping..." : "Stop Tracking"}
</button> </button>
)} )}
</div> </div>
<div className="divider my-1" /> <div className="divider my-1" />
</> </>
) : ( ) : (
@ -329,13 +320,25 @@ const Dashboard: React.FC = () => {
<p className="text-md font-bold">Projects</p> <p className="text-md font-bold">Projects</p>
</div> </div>
{/* 🔹 Table or Empty State */} {/* 🔹 Table or Empty State or Skeleton */}
{projectsData.length > 0 ? ( {isStopping ? (
// Skeleton loader for projects table
<div className="p-4 space-y-3">
{[...Array(3)].map((_, index) => (
<div key={index} className="flex items-center justify-between">
<div className="skeleton w-8 h-4"></div>
<div className="skeleton w-32 h-4"></div>
<div className="skeleton w-16 h-4"></div>
<div className="skeleton w-20 h-8 rounded"></div>
</div>
))}
</div>
) : projectsData.length > 0 ? (
<div <div
className="overflow-y-auto no-scrollbar" className="overflow-y-auto no-scrollbar"
style={{ style={{
maxHeight: "50vh", // scroll only after this height maxHeight: "50vh",
minHeight: "auto", // auto adjusts if few rows minHeight: "auto",
}} }}
> >
<table className="table table-zebra w-full text-[12px]"> <table className="table table-zebra w-full text-[12px]">
@ -358,20 +361,19 @@ const Dashboard: React.FC = () => {
return ( return (
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}> <tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
<td className="text-xs opacity-70">{index + 1}</td> <td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium"><div className="flex flex-col"> <td className="font-medium">
<div className="flex flex-col">
<span className="font-medium">{project.projectName}</span> <span className="font-medium">{project.projectName}</span>
{limitReached && ( {limitReached && (
<span className="text-xs text-error">Daily limit reached</span> <span className="text-xs text-error">Daily limit reached</span>
)} )}
</div></td> </div>
</td>
<td className="font-mono text-xs"> <td className="font-mono text-xs">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-mono text-xs"> <span className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)} {formatWorkTime(project.totalWorkTime)}
</span> </span>
{/* <span className="text-xs opacity-60">
of {project.dedicatedHours}
</span> */}
</div> </div>
</td> </td>
<td> <td>
@ -384,11 +386,6 @@ const Dashboard: React.FC = () => {
> >
{isCurrentlySelected ? "Selected" : "Select"} {isCurrentlySelected ? "Selected" : "Select"}
</button> </button>
{/* {!limitReached && (
<span className="text-xs text-success">
{remainingTime} left
</span>
)} */}
</div> </div>
</td> </td>
</tr> </tr>
@ -403,8 +400,6 @@ const Dashboard: React.FC = () => {
</div> </div>
)} )}
</div> </div>
</main> </main>
); );
}; };

View File

@ -23,16 +23,21 @@ const LeaveAndAdjustments: React.FC = () => {
try { try {
setIsSubmitting(true); setIsSubmitting(true);
console.log("Leave Payload:", payload); console.log("Leave /sick Payload:", payload);
const response = await window.electron.createPTOAndSickLeaveRequest(payload) const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:", response) console.log("Leave Payload:", response)
if (response.success) { if (response.success) {
return true; return { success: true };
} }
return {
success: false,
message: response.message
};
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return false; return { success: false, message: "Request failed" };
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -47,8 +52,7 @@ const LeaveAndAdjustments: React.FC = () => {
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100"> <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 ${ className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""
activeTab === "pto" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("pto")} onClick={() => setActiveTab("pto")}
> >
@ -57,8 +61,7 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${ className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""
activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")} onClick={() => setActiveTab("sick")}
> >
@ -67,8 +70,7 @@ const LeaveAndAdjustments: React.FC = () => {
<a <a
role="tab" role="tab"
className={`tab text-xs font-semibold ${ className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""
activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} } ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")} onClick={() => setActiveTab("adjustment")}
> >

View File

@ -1,5 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import ReactQuill from 'react-quill-new';
import { quillModules } from '../../../utils/quillConfig';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
interface Project { interface Project {
projectId: string; projectId: string;
@ -23,6 +27,7 @@ interface FormErrors {
selectedDate?: string; selectedDate?: string;
startTime?: string; startTime?: string;
endTime?: string; endTime?: string;
comment?: string;
general?: string; general?: string;
} }
@ -42,6 +47,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
const [endTime, setEndTime] = useState(""); const [endTime, setEndTime] = useState("");
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const quillRef = useRef<any>(null);
// Field validation functions // Field validation functions
const validateProjectId = (projectId: string): string | undefined => { const validateProjectId = (projectId: string): string | undefined => {
@ -120,6 +126,11 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value); error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
} }
break; break;
case 'comment':
if (!value.trim()) {
error = 'Description is required';
}
break;
} }
setFormErrors(prev => ({ setFormErrors(prev => ({
@ -138,6 +149,9 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
errors.selectedDate = validateDate(selectedDate); errors.selectedDate = validateDate(selectedDate);
errors.startTime = validateTime(startTime, 'Start time'); errors.startTime = validateTime(startTime, 'Start time');
errors.endTime = validateTime(endTime, 'End time'); errors.endTime = validateTime(endTime, 'End time');
if (!comment.trim()) {
errors.comment = 'Description is required';
}
if (!errors.startTime && !errors.endTime) { if (!errors.startTime && !errors.endTime) {
errors.startTime = validateTimeRange(startTime, endTime); errors.startTime = validateTimeRange(startTime, endTime);
@ -206,6 +220,107 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
} }
}; };
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
useEffect(() => { useEffect(() => {
if (selectedProjectId && selectedDate) { if (selectedProjectId && selectedDate) {
fetchLogs(); fetchLogs();
@ -283,6 +398,23 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
const handleDescription = (value: string) => { const handleDescription = (value: string) => {
setComment(value); setComment(value);
validateField('comment', value);
};
const resetForm = () => {
setSelectedProjectId("");
setSelectedDate("");
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
// Properly reset the Quill editor
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]); // Clear editor contents completely
}
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@ -321,14 +453,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
if (res?.success) { if (res?.success) {
console.log("Timesheet Created Successfully"); console.log("Timesheet Created Successfully");
alert(res.message); alert(res.message);
resetForm();
setSelectedProjectId("");
setSelectedDate("");
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
} else { } else {
setFormErrors(prev => ({ setFormErrors(prev => ({
...prev, ...prev,
@ -378,7 +503,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
</select> </select>
{formErrors.selectedProjectId && ( {formErrors.selectedProjectId && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span> <span className="label-text-alt text-error text-xs font-medium">{formErrors.selectedProjectId}</span>
</label> </label>
)} )}
</div> </div>
@ -396,7 +521,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
/> />
{formErrors.selectedDate && ( {formErrors.selectedDate && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span> <span className="label-text-alt text-error text-xs font-medium">{formErrors.selectedDate}</span>
</label> </label>
)} )}
</div> </div>
@ -460,7 +585,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
} }
{formErrors.startTime && ( {formErrors.startTime && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span> <span className="label-text-alt text-error text-xs font-medium">{formErrors.startTime}</span>
</label> </label>
)} )}
</div> </div>
@ -482,7 +607,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
} }
{formErrors.endTime && ( {formErrors.endTime && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span> <span className="label-text-alt text-error text-xs font-medium">{formErrors.endTime}</span>
</label> </label>
)} )}
</div> </div>
@ -502,18 +627,26 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
</div> </div>
</div> </div>
{/* Description */} {/* Description with ReactQuill */}
<div className="form-control flex flex-col gap-1"> <div className="form-control flex flex-col gap-1 col-span-2 mb-12">
<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 / Reason</span>
</label> </label>
<textarea <ReactQuill
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all" ref={quillRef}
placeholder="Please enter the task name or leave notes to help us better understand your request." theme="snow"
value={comment} value={comment}
onChange={(e) => handleDescription(e.target.value)} modules={quillModules}
onChange={handleDescription}
className="h-40"
// placeholder="Please enter the task name or leave notes to help us better understand your request."
/> />
</div> </div>
{formErrors.comment && (
<label className="label">
<span className="label-text-alt text-error text-xs font-medium">{formErrors.comment}</span>
</label>
)}
{/* Submit Button */} {/* Submit Button */}
<div className="flex gap-3"> <div className="flex gap-3">

View File

@ -1,5 +1,9 @@
// PtoLeaveTab.tsx // PtoLeaveTab.tsx
import React, { useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new";
import Quill from "quill";
import "quill/dist/quill.snow.css";
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -17,9 +21,114 @@ const PtoLeaveTab: React.FC<{
toDate: "", toDate: "",
comment: "", comment: "",
}); });
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null);
const [errors, setErrors] = useState<Partial<LeaveFormData>>({}); const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
// ... rest of your component code remains the same
const validate = () => { const validate = () => {
const e: Partial<LeaveFormData> = {}; const e: Partial<LeaveFormData> = {};
@ -29,26 +138,22 @@ const PtoLeaveTab: React.FC<{
const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null; const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null;
const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null; const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null;
// From date required
if (!from) { if (!from) {
e.fromDate = "From date is required"; e.fromDate = "From date is required";
} else if (from < today) { } else if (from < today) {
e.fromDate = "From date cannot be in the past"; e.fromDate = "From date cannot be in the past";
} }
// To date required
if (!to) { if (!to) {
e.toDate = "To date is required"; e.toDate = "To date is required";
} else if (to < today) { } else if (to < today) {
e.toDate = "To date cannot be in the past"; e.toDate = "To date cannot be in the past";
} }
// To date must be same or after From date
if (from && to && to < from) { if (from && to && to < from) {
e.toDate = "To date cannot be before From date"; e.toDate = "To date cannot be before From date";
} }
// Comment validation
if (!ptoLeave.comment.trim()) { if (!ptoLeave.comment.trim()) {
e.comment = "Comment required"; e.comment = "Comment required";
} }
@ -56,28 +161,56 @@ const PtoLeaveTab: React.FC<{
return e; return e;
}; };
const resetForm = () => {
const emptyForm: LeaveFormData = {
fromDate: "",
toDate: "",
comment: ""
};
setPtoLeave(emptyForm);
setErrors({});
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]);
}
};
const handleChange = (field: keyof LeaveFormData, value: string) => {
setPtoLeave(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
setServerError("")
const e = validate(); const e = validate();
if (Object.keys(e).length) return setErrors(e); if (Object.keys(e).length) return setErrors(e);
const success = await submitLeave({ const result = await submitLeave({
dateFrom: ptoLeave.fromDate, dateFrom: ptoLeave.fromDate,
dateTo: ptoLeave.toDate, dateTo: ptoLeave.toDate,
leaveType: "PTO", leaveType: "PTO",
comment: ptoLeave.comment, comment: ptoLeave.comment,
}); });
if (success) { if (result.success) {
setPtoLeave({ fromDate: "", toDate: "", comment: "" }); resetForm();
setErrors({});
alert("PTO submitted!"); alert("PTO submitted!");
} else {
setServerError(result.message || "Something went wrong");
} }
}; };
return ( return (
<>
<div className="max-w-4xl space-y-3 "> <div className="max-w-4xl space-y-3 ">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* From Date */} {/* From Date */}
<div className="form-control"> <div className="form-control">
<label className="label"> <label className="label">
@ -88,7 +221,7 @@ const PtoLeaveTab: React.FC<{
disabled={isSubmitting} disabled={isSubmitting}
value={ptoLeave.fromDate} value={ptoLeave.fromDate}
min={new Date().toISOString().split("T")[0]} min={new Date().toISOString().split("T")[0]}
onChange={(e) => setPtoLeave({ ...ptoLeave, fromDate: e.target.value })} onChange={(e) => handleChange('fromDate', e.target.value)}
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : "" className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
}`} }`}
/> />
@ -105,7 +238,7 @@ const PtoLeaveTab: React.FC<{
disabled={isSubmitting} disabled={isSubmitting}
value={ptoLeave.toDate} value={ptoLeave.toDate}
min={new Date().toISOString().split("T")[0]} min={new Date().toISOString().split("T")[0]}
onChange={(e) => setPtoLeave({ ...ptoLeave, toDate: e.target.value })} onChange={(e) => handleChange('toDate', e.target.value)}
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : "" className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
}`} }`}
/> />
@ -113,26 +246,30 @@ const PtoLeaveTab: React.FC<{
</div> </div>
{/* Comment */} {/* Comment */}
<div className="form-control col-span-2"> <div className="form-control mb-10 col-span-2">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm">Description</span> <span className="label-text font-semibold text-sm">Description</span>
</label> </label>
<textarea <ReactQuill
disabled={isSubmitting} ref={quillRef}
theme="snow"
value={ptoLeave.comment} value={ptoLeave.comment}
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation." modules={quillModules}
onChange={(e) => setPtoLeave({ ...ptoLeave, comment: e.target.value })} onChange={(val) => handleChange('comment', val)}
className={`textarea textarea-bordered text-xs h-40 ${errors.comment ? "textarea-error" : "" className="h-60"
}`}
/> />
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>} {errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div> </div>
</div> </div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
className="btn btn-primary flex-1" className="btn btn-primary flex-1 w-full"
> >
{isSubmitting ? ( {isSubmitting ? (
<span className="loading loading-spinner loading-sm"></span> <span className="loading loading-spinner loading-sm"></span>
@ -141,6 +278,7 @@ const PtoLeaveTab: React.FC<{
)} )}
</button> </button>
</div> </div>
</>
); );
}; };

View File

@ -1,4 +1,8 @@
import React, { useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new";
import Quill from "quill";
import "quill/dist/quill.snow.css"; // correct CSS
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -15,8 +19,111 @@ const SickLeaveTab: React.FC<{
toDate: '', toDate: '',
comment: '' comment: ''
}); });
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null);
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({}); const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => { const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
const errors: Partial<LeaveFormData> = {}; const errors: Partial<LeaveFormData> = {};
@ -46,7 +153,25 @@ const SickLeaveTab: React.FC<{
} }
}; };
const resetForm = () => {
const emptyForm: LeaveFormData = {
fromDate: "",
toDate: "",
comment: ""
};
setSickLeave(emptyForm);
setSickErrors({});
// Properly reset the Quill editor
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]); // Clear editor contents completely
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
setServerError("")
const errors = validateLeaveForm(sickLeave); const errors = validateLeaveForm(sickLeave);
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
@ -54,24 +179,23 @@ const SickLeaveTab: React.FC<{
return; return;
} }
const success = await submitLeave({ const result = await submitLeave({
dateFrom: sickLeave.fromDate, dateFrom: sickLeave.fromDate,
dateTo: sickLeave.toDate, dateTo: sickLeave.toDate,
leaveType: "SICK", leaveType: "SICK",
comment: sickLeave.comment, comment: sickLeave.comment,
}); });
if (success) { if (result.success) {
setSickLeave({ fromDate: "", toDate: "", comment: "" }); resetForm();
setSickErrors({});
alert("SICK submitted!"); alert("SICK submitted!");
} else {
setServerError(result.message || "Something went wrong");
} }
}; };
return ( return (
<>
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@ -118,31 +242,35 @@ const SickLeaveTab: React.FC<{
</div> </div>
{/* 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 mb-10">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-sm">Description</span> <span className="label-text font-semibold text-sm">Description</span>
</label> </label>
<textarea <ReactQuill
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${sickErrors.comment ? 'textarea-error' : '' ref={quillRef}
}`} theme="snow"
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} modules={quillModules}
onChange={(e) => handleSickChange('comment', e.target.value)} onChange={(val) => handleSickChange('comment', val)}
className="h-60"
/> />
</div>
{sickErrors.comment && ( {sickErrors.comment && (
<label className="label"> <label className="label">
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span> <span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
</label> </label>
)} )}
</div> </div>
</div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
{/* Submit Button */} {/* Submit Button */}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
className="btn btn-primary flex-1" className="btn btn-primary flex-1 w-full"
> >
{isSubmitting ? ( {isSubmitting ? (
<span className="loading loading-spinner loading-sm"></span> <span className="loading loading-spinner loading-sm"></span>
@ -152,6 +280,7 @@ const SickLeaveTab: React.FC<{
</button> </button>
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -0,0 +1,100 @@
const Schedule = () => {
const rows = [
{
period: "Oct 25, 2025 to Nov 07, 2025",
cutoff: "Nov 11, 2025 05:29:59 AM",
payDate: "Nov 14, 2025"
},
{
period: "Nov 08, 2025 to Nov 21, 2025",
cutoff: "Nov 25, 2025 05:29:59 AM",
payDate: "Nov 28, 2025"
},
{
period: "Nov 22, 2025 to Dec 05, 2025",
cutoff: "Dec 09, 2025 05:29:59 AM",
payDate: "Dec 12, 2025"
},
{
period: "Dec 06, 2025 to Dec 19, 2025",
cutoff: "Dec 23, 2025 05:29:59 AM",
payDate: "Dec 26, 2025"
},
{
period: "Dec 20, 2025 to Jan 02, 2026",
cutoff: "Jan 06, 2026 05:29:59 AM",
payDate: "Jan 09, 2026"
},
{
period: "Jan 03, 2026 to Jan 16, 2026",
cutoff: "Jan 20, 2026 05:29:59 AM",
payDate: "Jan 23, 2026"
},
{
period: "Jan 17, 2026 to Jan 30, 2026",
cutoff: "Feb 03, 2026 05:29:59 AM",
payDate: "Feb 06, 2026"
},
{
period: "Jan 31, 2026 to Feb 13, 2026",
cutoff: "Feb 17, 2026 05:29:59 AM",
payDate: "Feb 20, 2026"
},
{
period: "Feb 14, 2026 to Feb 27, 2026",
cutoff: "Mar 03, 2026 05:29:59 AM",
payDate: "Mar 06, 2026"
},
{
period: "Feb 28, 2026 to Mar 13, 2026",
cutoff: "Mar 17, 2026 05:29:59 AM",
payDate: "Mar 20, 2026"
}
];
return (
<>
<div className="flex flex-col gap-2 max-h-screen">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-md font-bold">Schedules</h2>
</div>
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh] no-scrollbar">
{/* ⭐ FIXED: Horizontal scroll wrapper */}
<div className="overflow-x-auto w-full no-scrollbar">
<table className="table table-zebra w-full text-[12px] min-w-[600px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Period</th>
<th>Cutoff Time</th>
<th>Pay Date</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{row.period}</td>
<td>
{row.cutoff}
<br />
<span className="text-gray-500 text-[11px]">(Asia/Calcutta)</span>
</td>
<td>{row.payDate}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</>
);
};
export default Schedule;

View File

@ -70,7 +70,7 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
<td>{tabIdx + 1}</td> <td>{tabIdx + 1}</td>
<td className="truncate max-w-[150px]" title={tab.title}> <td className="truncate max-w-[150px]" title={tab.title}>
{tab.title} <br/> {tab.title} <br/>
{tab.appName} <span className="text-slate-500">{tab.appName}</span>
</td> </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><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>

View File

@ -113,10 +113,11 @@ const projectsSlice = createSlice({
} }
}, },
// In your projectsSlice.ts
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) { stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
const p = state.projectTrackingMap[action.payload.projectId]; const p = state.projectTrackingMap[action.payload.projectId];
if (p) { if (p) {
p.accumulatedTime = p.elapsedTime; // Only update tracking status, time will be updated from server
p.isTracking = false; p.isTracking = false;
p.isPaused = false; p.isPaused = false;
p.startTime = null; p.startTime = null;