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

298 lines
9.2 KiB
TypeScript

import React, { useEffect, useState } from "react";
import {
CalendarDays,
Clock,
FileText,
CheckCircle,
XCircle,
Eye,
EyeOff,
RefreshCw,
Loader2
} from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import ProjectLogsModal from "./ProjectLogsModal";
import { TrackingState } from "../../layout/DashboardLayout";
import { useOutletContext } from "react-router-dom";
import { DateTime } from "luxon";
interface TimesheetEntry {
id: number;
date: string;
project: string;
hours: number;
status: "Approved" | "Pending" | "Rejected";
remarks?: string;
}
const timesheetData: TimesheetEntry[] = [
{ id: 1, date: "2025-10-28", project: "Employee Tracker", hours: 8, status: "Approved", remarks: "Full day" },
{ id: 2, date: "2025-10-29", project: "CRM Upgrade", hours: 6, status: "Pending", remarks: "Client call delays" },
{ id: 3, date: "2025-10-30", project: "Sales Dashboard", hours: 7, status: "Rejected", remarks: "Missing details" },
{ id: 4, date: "2025-10-31", project: "Finance Tracker", hours: 9, status: "Approved", remarks: "Overtime" },
{ id: 5, date: "2025-11-01", project: "AI Model Trainer", hours: 8, status: "Approved", remarks: "Normal day" },
];
interface Project {
projectId: string;
projectName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
status: string
}
const Timesheets: React.FC = () => {
const { isTracking } = useOutletContext<TrackingState>();
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]);
// const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0);
const { selectedProject } = useSelector((state: any) => state.projects);
const selectedProjectId = selectedProject?.projectId ?? null;
const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
useEffect(() => {
const interval = setInterval(() => {
setTick((t) => t + 1); // force re-render every second
}, 1000);
return () => clearInterval(interval);
}, []);
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
};
const fetchProjects = async () => {
setInitialLoading(true);
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
setLastRefreshed(new Date());
} else {
console.warn(" Unexpected response:", res);
}
} catch (err) {
console.error("Error fetching projects:", err);
} finally {
setInitialLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleRefresh = async () => {
setRefreshing(true);
await fetchProjects();
};
const handleViewLogs = async (projectID: string, projectName: string) => {
try {
setModalOpen(true);
setLoading(true);
// setSelectedProject(projectID);
setSelectedProjectName(projectName);
console.log("projectID", projectID)
const res = await window.electron.getProjectLogs(projectID);
console.log("📦 Full Logs:", res);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0].sessions || []);
} else {
setLogs([]);
}
} catch (error) {
console.error("Error fetching logs:", error);
setModalOpen(false)
} finally {
setLoading(false);
}
};
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(" ");
};
// Skeleton loader rows
const renderSkeletonRows = () => {
return Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td>
<div className="skeleton h-4 w-6 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-32 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-16 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-8 w-8 rounded bg-base-300"></div>
</td>
</tr>
));
};
return (
<main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
>
{/* Header */}
<div className="flex-none mb-2 flex justify-between items-center">
<h2 className="text-md font-bold">Timesheets</h2>
</div>
{/* Timesheet Table */}
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
style={{
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Project</th>
<th>Hours</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
{initialLoading ? (
// Show skeleton while initially loading
renderSkeletonRows()
) : refreshing && projectsData.length === 0 ? (
// Show skeleton while refreshing with empty data
renderSkeletonRows()
) : (
// Show actual data
projectsData.map((entry, index) => (
<tr
key={entry.projectId}
className={`${entry.status === "INACTIVE" && "hidden"}`}
>
<td>{index + 1}</td>
<td>{entry.projectName}</td>
<td>{formatWorkTime(entry.totalWorkTime)}</td>
<td>
<button
disabled={(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId))}
onClick={() =>
handleViewLogs(entry.projectId, entry.projectName)
}
className="btn btn-ghost btn-xs"
>
{(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId)) ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</td>
</tr>
))
)}
{/* {refreshing && projectsData.length > 0 && (
// Show a single skeleton row at the top while refreshing with existing data
<tr>
<td colSpan={4} className="bg-base-200/50">
<div className="flex justify-center py-2">
<Loader2 size={20} className="animate-spin" />
<span className="ml-2 text-sm">Refreshing...</span>
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* Footer Summary */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh projects"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div>
<ProjectLogsModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
projectName={selectedProjectName}
logs={logs}
loading={loading}
/>
</main>
);
};
export default Timesheets;