298 lines
9.2 KiB
TypeScript
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; |