316 lines
10 KiB
TypeScript
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; |