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

316 lines
10 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";
// Updated Project interface to match JSON structure
interface Project {
projectId: string;
projectName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
}
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[];
}
// 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 {
selectedProject,
projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
startTime,
handleStartTracking,
handlePauseResume,
handleStopTracking,
handleProjectSelect,
projectsData,
} = useOutletContext<TrackingState>();
// interface AppsListInt {
// name: string;
// pid: string;
// title: string;
// }
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.projectId)
: null;
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(" ");
};
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>
{/* <button
onClick={async () => {
await window.electron.uploadScreenshot();
}}
className="btn btn-xs btn-outline mt-2"
>
Screenshot Test
</button> */}
</div>
{/* Widgets */}
{/* <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Widget
title="Logged Hours Today"
value="6h 24m"
icon={<Clock size={24} />}
color="primary"
/>
<Widget
title="Logged Hours This Week"
value="4h 48m"
icon={<TrendingUp size={24} />}
color="success"
/>
<Widget
title="Active Project"
value={selectedProject ? selectedProject.projectName : "—"}
icon={<ProjectorIcon size={24} />}
color="warning"
/>
</div> */}
{/* Tracking Section */}
<div className="w-full mb-4">
{/* <div className="flex justify-between items-center mb-2">
<span className="text-md font-bold">Tracking</span>
</div> */}
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body">
{selectedProject ? (
<>
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-center sm:text-left">
<p className="text-lg font-extrabold text-success">
{formatTime(selectedData?.elapsedTime || 0)}
</p>
{/* <p className="text-xs text-base-content/70 mt-1">
{selectedData?.isTracking ? "Current Session" : "Ready"}
</p> */}
<div>
<span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold">
{selectedProject.projectName}
</span>
</div>
</div>
{!selectedData?.isTracking ? (
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
>
<Play className="w-3 h-3" /> Start Tracking
</button>
) : (
<button
className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking}
>
<Square className="w-3 h-3" />
Stop Tracking
</button>
)}
</div>
<div className="divider my-1" />
{/* <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${selectedProject.activityStatus === "ACTIVE"
? "badge-success"
: "badge-warning"
}`}
>
{selectedProject.activityStatus}
</span>
</div>
<div>
<span className="text-base-content/70">Tracking:</span>{" "}
<span
className={`badge ${selectedData?.isPaused
? "badge-warning"
: selectedData?.isTracking
? "badge-success"
: "badge-neutral"
}`}
>
{selectedData?.isPaused
? "Paused"
: selectedData?.isTracking
? "Active"
: "Ready"}
</span>
</div>
<div>
<span className="text-base-content/70">Start:</span>{" "}
<span>
{selectedData?.startTime
? selectedData.startTime.toLocaleTimeString()
: "—"}
</span>
</div>
</div> */}
</>
) : (
<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 */}
{projectsData.length > 0 ? (
<div
className="overflow-y-auto"
style={{
maxHeight: "50vh", // scroll only after this height
minHeight: "auto", // auto adjusts if few rows
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Name</th>
<th>Logs Hours</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project, index) => {
const isCurrentlySelected =
selectedProject?.projectId === project.projectId;
const isDisabled = isTracking && !isCurrentlySelected;
return (
<tr key={project.projectId}>
<td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">{project.projectName}</td>
<td className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)}
</td>
<td>
<button
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelect(project.projectId)}
disabled={isDisabled}
>
{isCurrentlySelected ? "Selected" : "Select"}
</button>
</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;