Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { TimeLog, timeLogApi } from "@/services/timeLogApi"; | |
| import { Employee, employeeApi } from "@/services/employeeApi"; | |
| import { format } from "date-fns"; | |
| import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; | |
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Clock, Loader2, User } from "lucide-react"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { useQuery } from "@tanstack/react-query"; | |
| interface TaskTimeLogCardProps { | |
| taskId: number; | |
| entityType?: "Task" | "Issue"; | |
| } | |
| const TaskTimeLogCard: React.FC<TaskTimeLogCardProps> = ({ | |
| taskId, | |
| entityType = "Task" | |
| }) => { | |
| const [timeLogs, setTimeLogs] = useState<TimeLog[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| // Fetch employees data | |
| const { | |
| data: employees = [], | |
| isLoading: isLoadingEmployees | |
| } = useQuery({ | |
| queryKey: ["employees"], | |
| queryFn: () => employeeApi.getAll(), | |
| }); | |
| useEffect(() => { | |
| const fetchTimeLogs = async () => { | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| let logs: TimeLog[] = []; | |
| if (entityType === "Issue") { | |
| logs = await timeLogApi.getTimeLogByIssueID(taskId); | |
| } else { | |
| logs = await timeLogApi.getTimeLogByTaskID(taskId); | |
| } | |
| setTimeLogs(logs); | |
| } catch (err) { | |
| console.error(`Error fetching time logs for ${entityType}:`, err); | |
| setError(`Failed to load time logs for this ${entityType.toLowerCase()}. Please try again later.`); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| if (taskId) { | |
| fetchTimeLogs(); | |
| } | |
| }, [taskId, entityType]); | |
| // Format date for display | |
| const formatDate = (dateString: string) => { | |
| try { | |
| return format(new Date(dateString), "MMM dd, yyyy"); | |
| } catch (error) { | |
| return "Invalid date"; | |
| } | |
| }; | |
| // Utility to strip HTML tags from text for tooltips | |
| const stripHtmlTags = (html: string): string => { | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = html; | |
| return tempDiv.textContent || tempDiv.innerText || ''; | |
| }; | |
| // Helper to get employee name by userId | |
| const getEmployeeNameByUserId = (userId: string): string => { | |
| const employee = employees.find(emp => emp.userId.toString() === userId); | |
| if (employee) { | |
| return `${employee.firstName} ${employee.lastName}`; | |
| } | |
| return userId; | |
| }; | |
| // Calculate total hours spent | |
| const totalHoursSpent = timeLogs.reduce((total, log) => total + log.hrsSpent, 0); | |
| // Mobile view time log card | |
| const renderMobileTimeLog = (log: TimeLog) => ( | |
| <div key={log.id} className="border rounded-md p-3 mb-2 text-sm"> | |
| <div className="flex justify-between items-start mb-2"> | |
| <Badge variant="outline" className="font-mono bg-indigo-50 text-indigo-700 border-indigo-200 px-2 py-0.5"> | |
| {log.startTime} - {log.endTime} | |
| </Badge> | |
| <Badge variant="secondary" className="font-medium"> | |
| {log.hrsSpent.toFixed(2)} hrs | |
| </Badge> | |
| </div> | |
| <div className="mb-2"> | |
| <p className="text-xs text-muted-foreground mb-1">Date</p> | |
| <div className="flex items-center"> | |
| <Clock className="h-3 w-3 text-muted-foreground mr-1" /> | |
| <span>{formatDate(log.workDate)}</span> | |
| </div> | |
| </div> | |
| <div className="border-t pt-2 mt-2"> | |
| <p className="text-xs text-muted-foreground mb-1">Comments</p> | |
| <div className="text-sm line-clamp-3" title={stripHtmlTags(log.comments || "No comments")} dangerouslySetInnerHTML={{ __html: log.comments || "No comments" }} /> | |
| </div> | |
| <div className="text-xs text-muted-foreground mt-2 text-right"> | |
| <div className="flex items-center justify-end gap-1"> | |
| <Avatar className="h-5 w-5"> | |
| <AvatarFallback className="bg-primary/10 text-primary text-xs"> | |
| {getEmployeeNameByUserId(log.createdBy)?.slice(0, 2).toUpperCase() || 'U'} | |
| </AvatarFallback> | |
| </Avatar> | |
| <span>Logged by {getEmployeeNameByUserId(log.createdBy)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| return ( | |
| <Card className="border shadow-sm"> | |
| <CardHeader className="pb-2 sm:pb-4"> | |
| <CardTitle className="text-lg sm:text-xl flex items-center"> | |
| <Clock className="mr-2 h-4 w-4 sm:h-5 sm:w-5" /> | |
| Time Logs | |
| </CardTitle> | |
| <CardDescription> | |
| Time spent on this {entityType.toLowerCase()}: {totalHoursSpent.toFixed(2)} hours | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| {isLoading || isLoadingEmployees ? ( | |
| <div className="flex justify-center items-center py-6"> | |
| <Loader2 className="h-6 w-6 animate-spin text-primary" /> | |
| <span className="ml-2">Loading time logs...</span> | |
| </div> | |
| ) : error ? ( | |
| <div className="text-center text-red-500 py-4">{error}</div> | |
| ) : timeLogs.length === 0 ? ( | |
| <div className="text-center text-muted-foreground py-6"> | |
| No time logs recorded for this {entityType.toLowerCase()}. | |
| </div> | |
| ) : ( | |
| <> | |
| {/* Mobile view - Card layout */} | |
| <div className="sm:hidden"> | |
| {timeLogs.map(renderMobileTimeLog)} | |
| </div> | |
| {/* Desktop view - Table layout */} | |
| <div className="hidden sm:block overflow-x-auto"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Date</TableHead> | |
| <TableHead>Time Range</TableHead> | |
| <TableHead>Hours</TableHead> | |
| <TableHead>Comments</TableHead> | |
| <TableHead>Logged By</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {timeLogs.map((log) => ( | |
| <TableRow key={log.id}> | |
| <TableCell> | |
| {formatDate(log.workDate)} | |
| </TableCell> | |
| <TableCell> | |
| <Badge variant="outline" className="font-mono bg-indigo-50 text-indigo-700 border-indigo-200 px-2 py-0.5"> | |
| {log.startTime} - {log.endTime} | |
| </Badge> | |
| </TableCell> | |
| <TableCell> | |
| <span className="font-medium">{log.hrsSpent.toFixed(2)}</span> | |
| </TableCell> | |
| <TableCell className="max-w-xs"> | |
| <div className="truncate" title={stripHtmlTags(log.comments || "No comments")} dangerouslySetInnerHTML={{ __html: log.comments || "No comments" }} /> | |
| </TableCell> | |
| <TableCell> | |
| <div className="flex items-center gap-2"> | |
| <Avatar className="h-7 w-7"> | |
| <AvatarFallback className="bg-gradient-to-br from-indigo-500 to-purple-500 text-white text-xs"> | |
| {getEmployeeNameByUserId(log.createdBy)?.slice(0, 2).toUpperCase() || 'U'} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="text-sm font-medium text-gray-800"> | |
| {getEmployeeNameByUserId(log.createdBy)} | |
| </div> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| </> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| export default TaskTimeLogCard; |