va-tracker/src/ui/pages/Dashboard.tsx

407 lines
14 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom";
import NoData from "./NoData";
import { useDispatch, useSelector } from "react-redux";
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice";
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
import { parseTimeStringToSeconds } from "../utils/timeParser";
// Updated Project interface to match JSON structure
interface Project {
projectId: string;
projectName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
status: string
}
interface ProjectTrackingData {
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
isTracking: boolean;
isPaused: boolean;
}
interface TrackingState {
selectedProject: Project | null;
// projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (projectId: string) => void;
projectsData: Project[];
loadingStart: boolean,
loadingStop: boolean,
isStopping: boolean
}
// Helper function
const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hrs}h ${mins}m ${secs}s`;
// return `${hrs}h ${mins}m `;
};
// Widget Component
const Widget: React.FC<{
title: string;
value: string | number;
subtitle?: string;
icon?: React.ReactNode;
color?:
| "primary"
| "secondary"
| "accent"
| "success"
| "warning"
| "error"
| "info";
}> = ({ title, value, subtitle, icon, color = "primary" }) => (
<div className="card bg-base-100 shadow-sm hover:shadow-lg transition-shadow">
<div className="card-body">
<div className="flex items-start justify-between">
<div>
<h3 className="font-medium text-base-content/60">{title}</h3>
<p className="text-sm font-bold mt-2">{value}</p>
</div>
<div className={`p-3 bg-${color}/10 rounded-lg`}>
<div className={`text-${color}`}>{icon}</div>
</div>
</div>
{subtitle && <p className="text-xs text-base-content/60 mt-2">{subtitle}</p>}
</div>
</div>
);
// Dashboard Component
const Dashboard: React.FC = () => {
const {
isTracking,
isPaused,
elapsedTime,
startTime,
handleStartTracking,
handlePauseResume,
handleStopTracking,
handleProjectSelect,
loadingStart,
loadingStop,
isStopping
} = useOutletContext<TrackingState>();
const projectsData = useSelector((state: any) => state.projects.projectsData);
const selectedProject = useSelector((state: any) => state.projects.selectedProject);
const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
const dispatch = useDispatch();
const fetchProfile = async () => {
try {
dispatch(setLoading(true));
const res = await window.electron.getProfile();
console.log("Profile", res)
if (res.success && res.data) {
dispatch(setProfile(res.data));
} else {
dispatch(setError("Failed to fetch profile"));
}
} catch (err: any) {
dispatch(setError(err.message));
} finally {
dispatch(setLoading(false));
}
};
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList();
console.log(" Project list response in dashboard:", res);
if (res?.success && Array.isArray(res.data)) {
// Store data redux store
dispatch(setProjectsDataSlice(res.data));
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 || "",
})
);
// Auto-select first project
if (res.data.length > 0 && !selectedProject) {
const firstProject = res.data[0];
// Select the project
dispatch(setSelectedProjectSlice(firstProject));
const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime);
// Initialize tracking data for first project
dispatch(
initializeProjectTracking({
projectId: firstProject.projectId,
initialTime: initialElapsed,
})
);
}
fetchProfile()
} else {
console.warn("⚠️ Unexpected response:", res);
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const selectedData = selectedProject
? projectTrackingMap[selectedProject.projectId]
: 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) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
const handleProjectSelectLocal = (projectId: string) => {
const project = projectsData.find((p: any) => p.projectId === projectId);
if (!project) return;
dispatch(setSelectedProjectSlice(project));
const existingData = projectTrackingMap[projectId];
if (!existingData) {
const initialElapsed = parseTimeStringToSeconds(project.totalWorkTime);
dispatch(initializeProjectTracking({
projectId,
initialTime: initialElapsed
}));
} else {
// Restore saved tracking state
dispatch(updateProjectTracking({
projectId,
data: existingData
}));
}
};
return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
{/* Header */}
<div className="mb-2">
<h1 className="text-md font-bold">Dashboard</h1>
</div>
{/* Tracking Section */}
<div className="w-full mb-4">
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body">
{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="text-center sm:text-left">
{/* FIX: Use displayTime instead of always using server time */}
<p className="text-lg font-extrabold text-success">
{formatTime(displayTime)}
</p>
<div>
<span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold">
{selectedProject.projectName}
</span>
</div>
{/* Show tracking status */}
{selectedData?.isTracking && (
<div className="text-xs text-success mt-1">
● Live Tracking
</div>
)}
</div>
{!selectedData?.isTracking ? (
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
disabled={loadingStart}
>
{loadingStart ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<Play className="w-3 h-3" />
)}
{loadingStart ? "Starting..." : "Start Tracking"}
</button>
) : (
<button
className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking}
disabled={loadingStop}
>
{loadingStop ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<Square className="w-3 h-3" />
)}
{loadingStop ? "Stopping..." : "Stop Tracking"}
</button>
)}
</div>
<div className="divider my-1" />
</>
) : (
<p className="text-sm text-base-content/60 text-center">
Select a project below to start tracking.
</p>
)}
</div>
</div>
</div>
{/* Projects Table */}
<div className="border border-base-content/10 bg-base-100 shadow-md min-h-[58vh] flex flex-col">
{/* 🔹 Sticky Header */}
<div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
<p className="text-md font-bold">Projects</p>
</div>
{/* 🔹 Table or Empty State or Skeleton */}
{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
className="overflow-y-auto no-scrollbar"
style={{
maxHeight: "50vh",
minHeight: "auto",
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Name</th>
<th>Logs Hours</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project: any, index: any) => {
const isCurrentlySelected =
selectedProject?.projectId === project.projectId;
const isDisabled = isTracking && !isCurrentlySelected;
const limitReached = isProjectLimitReached(project);
const remainingTime = getFormattedRemainingTime(project);
return (
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
<td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">
<div className="flex flex-col">
<span className="font-medium">{project.projectName}</span>
{limitReached && (
<span className="text-xs text-error">Daily limit reached</span>
)}
</div>
</td>
<td className="font-mono text-xs">
<div className="flex flex-col">
<span className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)}
</span>
</div>
</td>
<td>
<div className="flex flex-col gap-1">
<button
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelectLocal(project.projectId)}
disabled={isDisabled || limitReached}
>
{isCurrentlySelected ? "Selected" : "Select"}
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<div className="flex items-center justify-center flex-1 text-sm text-base-content/60">
<NoData />
</div>
)}
</div>
</main>
);
};
export default Dashboard;