pmtool / src /components /tasks /TaskMatrixView.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
25.4 kB
import React, { useState, useMemo } from 'react';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay } from 'date-fns';
import { Task } from '@/services/tasksApi';
import { Employee } from '@/types';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, X, Calendar as CalendarIcon, User, ListFilter, Clock } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { useNavigate } from "react-router-dom";
import { cn } from "@/lib/utils";
import { CustomSheet } from "@/components/ui/custom-sheet";
interface TaskMatrixViewProps {
tasks: Task[];
employees: Employee[];
}
interface SelectionState {
type: 'cell' | 'employee' | 'day';
id: string; // Unique identifier for highlighting
title: string;
subTitle?: string;
tasks: Task[];
}
export const TaskMatrixView: React.FC<TaskMatrixViewProps> = ({ tasks, employees }) => {
const navigate = useNavigate();
const [currentDate, setCurrentDate] = useState(new Date());
const [selection, setSelection] = useState<SelectionState | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
// Get all days in the current month
const daysInMonth = useMemo(() => {
const start = startOfMonth(currentDate);
const end = endOfMonth(currentDate);
return eachDayOfInterval({ start, end });
}, [currentDate]);
// Navigate months
const nextMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
setSelection(null);
};
const prevMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
setSelection(null);
};
// Calculate matrix data (counts and tasks per cell)
const matrixData = useMemo(() => {
const data: Record<number, Record<string, Task[]>> = {};
const dailyTotals: Record<string, number> = {};
const dailyTasks: Record<string, Task[]> = {}; // Store all tasks for a day
const employeeTasks: Record<number, Task[]> = {}; // Store all monthly tasks for an employee
// Initialize daily totals
daysInMonth.forEach(day => {
const dayStr = format(day, 'yyyy-MM-dd');
dailyTotals[dayStr] = 0;
dailyTasks[dayStr] = [];
});
// Initialize employee rows
employees.forEach(emp => {
data[emp.id] = {};
employeeTasks[emp.id] = [];
daysInMonth.forEach(day => {
const dayStr = format(day, 'yyyy-MM-dd');
data[emp.id][dayStr] = [];
});
});
// Loop through each task
tasks.forEach(task => {
if (!task.assignedTo) return;
// Skip tasks for employees not in the current list
if (!data[task.assignedTo]) return;
const startDate = task.stateDate ? parseISO(task.stateDate) : (task.createdAt ? parseISO(task.createdAt) : new Date());
let endDate = task.endDate ? parseISO(task.endDate) : startDate;
if (endDate < startDate) endDate = startDate;
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
if (endDate < monthStart || startDate > monthEnd) return;
// Track if we've added this task to the employee's monthly list already
let addedToEmployee = false;
daysInMonth.forEach(day => {
const dayStr = format(day, 'yyyy-MM-dd');
const currentDayStart = startOfDay(day);
const rangeStart = startOfDay(startDate);
const rangeEnd = endOfDay(endDate);
if (currentDayStart >= rangeStart && currentDayStart <= rangeEnd) {
if (data[task.assignedTo] && data[task.assignedTo][dayStr] !== undefined) {
data[task.assignedTo][dayStr].push(task);
dailyTotals[dayStr] = (dailyTotals[dayStr] || 0) + 1;
// Add to day aggregate (ensure uniqueness if needed, but tasks are unique per iteration)
dailyTasks[dayStr].push(task);
// Add to employee aggregate
if (!addedToEmployee) {
employeeTasks[task.assignedTo].push(task);
addedToEmployee = true;
}
}
}
});
});
return { employeeData: data, dailyTotals, dailyTasks, employeeTasks };
}, [tasks, employees, daysInMonth, currentDate]);
// Handle selection interactions
const handleCellClick = (employee: Employee, day: Date, tasks: Task[]) => {
if (tasks.length > 0) {
setSelection({
type: 'cell',
id: `${employee.id}-${format(day, 'yyyy-MM-dd')}`,
title: `Tasks for ${employee.firstName} ${employee.lastName}`,
subTitle: format(day, "EEEE, MMMM do, yyyy"),
tasks: tasks
});
}
};
const handleEmployeeHeaderClick = (employee: Employee) => {
const tasks = matrixData.employeeTasks[employee.id] || [];
if (tasks.length > 0) {
setSelection({
type: 'employee',
id: employee.id.toString(),
title: `Tasks for ${employee.firstName} ${employee.lastName}`,
subTitle: `Total for ${format(currentDate, 'MMMM yyyy')}`,
tasks: tasks
});
}
};
const handleDayHeaderClick = (day: Date) => {
const dayStr = format(day, 'yyyy-MM-dd');
const tasks = matrixData.dailyTasks[dayStr] || [];
if (tasks.length > 0) {
setSelection({
type: 'day',
id: dayStr,
title: `All Tasks for ${format(day, "MMMM do")}`,
subTitle: format(day, "EEEE, yyyy"),
tasks: tasks
});
}
};
const handleEditTask = (task: Task) => {
setSelectedTask(task);
setIsSheetOpen(true);
};
return (
<div className="space-y-6">
<Card className="w-full shadow-sm border-none">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-bold">Monthly employee and day wise task report</CardTitle>
<div className="flex items-center space-x-2">
<Button variant="outline" size="icon" onClick={prevMonth}>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="min-w-[150px] text-center font-medium">
{format(currentDate, 'MMMM yyyy')}
</span>
<Button variant="outline" size="icon" onClick={nextMonth}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{/* Key change: Added max-height and scrolling to the container */}
<div className="max-h-[600px] overflow-auto relative border rounded-md m-4">
<table className="w-full caption-bottom text-sm text-left">
<TableHeader className="bg-muted z-30 sticky top-0 shadow-sm">
<TableRow>
<TableHead className="w-[200px] sticky left-0 top-0 z-50 bg-muted border-r shadow-[1px_0_0_0_rgba(0,0,0,0.1)]">
Employee Name
</TableHead>
{daysInMonth.map((day) => {
const dayStr = format(day, 'yyyy-MM-dd');
const isSelected = selection?.type === 'day' && selection.id === dayStr;
return (
<TableHead
key={day.toString()}
className={cn(
"text-center min-w-[40px] px-1 border-r border-b font-bold cursor-pointer hover:bg-muted/80 transition-colors sticky top-0 z-30 bg-muted",
isSelected ? "bg-primary/20 text-primary" : "text-foreground"
)}
onClick={() => handleDayHeaderClick(day)}
>
<div className="flex flex-col items-center justify-center py-1">
<span className="text-[10px] font-normal uppercase text-muted-foreground">{format(day, 'EEE')}</span>
<span>{format(day, 'd')}</span>
</div>
</TableHead>
);
})}
</TableRow>
</TableHeader>
<TableBody>
{employees.map((employee) => {
const isRowSelected = selection?.type === 'employee' && selection.id === employee.id.toString();
return (
<TableRow key={employee.id} className={isRowSelected ? "bg-primary/5" : ""}>
<TableCell
className={cn(
"font-medium sticky left-0 z-20 bg-background border-r border-b shadow-[1px_0_0_0_rgba(0,0,0,0.1)] cursor-pointer hover:bg-muted/50 transition-colors",
isRowSelected ? "bg-primary/10 text-primary" : ""
)}
onClick={() => handleEmployeeHeaderClick(employee)}
>
<div className="truncate w-[180px]" title={`${employee.firstName} ${employee.lastName}`}>
{employee.firstName} {employee.lastName}
</div>
</TableCell>
{daysInMonth.map((day) => {
const dayStr = format(day, 'yyyy-MM-dd');
const cellTasks = matrixData.employeeData[employee.id]?.[dayStr] || [];
const count = cellTasks.length;
const cellId = `${employee.id}-${dayStr}`;
// Determine highlighting based on selection type
const isCellSelected = selection?.type === 'cell' && selection.id === cellId;
const isColSelected = selection?.type === 'day' && selection.id === dayStr;
// Base background color logic
let bgClass = "";
if (isCellSelected) bgClass = "bg-primary/20 ring-1 ring-primary ring-inset z-10";
else if (isRowSelected || isColSelected) bgClass = "bg-primary/5";
return (
<TableCell
key={dayStr}
className={cn(
"text-center border-r border-b p-0 relative transition-colors h-10 w-10",
count > 0 ? "cursor-pointer hover:bg-primary/10" : "",
bgClass
)}
onClick={() => count > 0 && handleCellClick(employee, day, cellTasks)}
>
<div className="h-full w-full flex items-center justify-center">
{count > 0 ? (
<span className={cn(
"font-medium",
isCellSelected ? "text-primary font-bold" : ""
)}>
{count}
</span>
) : (
<span className="text-muted-foreground/30 text-xs">-</span>
)}
</div>
</TableCell>
);
})}
</TableRow>
);
})}
{/* Totals Row */}
<TableRow className="font-bold bg-muted/30 sticky bottom-0 z-30 shadow-[0_-1px_0_0_rgba(0,0,0,0.1)]">
<TableCell
className="sticky left-0 z-40 bg-muted border-r border-t shadow-[1px_0_0_0_rgba(0,0,0,0.1)]"
>
Total Task
</TableCell>
{daysInMonth.map((day) => {
const dayStr = format(day, 'yyyy-MM-dd');
return (
<TableCell key={dayStr} className="text-center border-t border-r bg-muted/30 p-2 text-xs">
{matrixData.dailyTotals[dayStr] || 0}
</TableCell>
);
})}
</TableRow>
</TableBody>
</table>
</div>
</CardContent>
</Card>
{/* Selected Task List */}
{selection && (
<Card className="animate-in slide-in-from-top-4 duration-300 border-l-4 border-l-primary shadow-md">
<CardHeader className="pb-2 bg-muted/20">
<div className="flex justify-between items-center">
<div>
<CardTitle className="text-lg flex items-center gap-2">
{selection.type === 'employee' && <User className="h-5 w-5 text-primary" />}
{selection.type === 'day' && <CalendarIcon className="h-5 w-5 text-primary" />}
{selection.type === 'cell' && <ListFilter className="h-5 w-5 text-primary" />}
{selection.title}
</CardTitle>
{selection.subTitle && (
<p className="text-sm text-muted-foreground mt-1 flex items-center">
{selection.subTitle}
</p>
)}
</div>
<Button variant="ghost" size="icon" onClick={() => setSelection(null)}>
<X className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="pt-4 px-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="pl-6">Title</TableHead>
<TableHead>Status</TableHead>
<TableHead>Start Date</TableHead>
<TableHead className="pr-6">End Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selection.tasks.map((task) => (
<TableRow
key={`${task.id}-${task.assignedTo}`}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleEditTask(task)}
>
<TableCell className="font-medium pl-6 max-w-[300px] truncate" title={task.title}>{task.title}</TableCell>
<TableCell>
<Badge variant="outline" className={cn(
task.status === "Completed" ? "bg-green-100 text-green-700 border-green-200" :
task.status === "In Progress" ? "bg-blue-100 text-blue-700 border-blue-200" : ""
)}>{task.status}</Badge>
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{task.stateDate ? format(parseISO(task.stateDate), 'MMM d, yyyy') : '-'}
</TableCell>
<TableCell className="text-muted-foreground text-sm pr-6">
{task.endDate ? format(parseISO(task.endDate), 'MMM d, yyyy') : '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
{/* Task Details Sheet - REPLACED WITH CUSTOM SHEET */}
<CustomSheet
open={isSheetOpen}
onOpenChange={setIsSheetOpen}
title={selectedTask?.title}
description={selectedTask ? `Task ID: #${selectedTask.id}` : undefined}
className="w-[400px] sm:w-[540px]"
>
{selectedTask && (
<div className="space-y-6">
<div className="space-y-6">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className={cn(
"px-2 py-1 text-sm font-medium",
selectedTask.status === "Completed" ? "bg-green-100 text-green-700 border-green-200" :
selectedTask.status === "In Progress" ? "bg-blue-100 text-blue-700 border-blue-200" :
"bg-gray-100 text-gray-700 border-gray-200"
)}>
{selectedTask.status}
</Badge>
<Badge variant="secondary" className="px-2 py-1 text-sm">
{selectedTask.type}
</Badge>
<Badge variant="outline" className="px-2 py-1 text-sm">
Priority: {selectedTask.priority}
</Badge>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<CalendarIcon className="h-4 w-4" /> Start Date
</h4>
<p className="font-medium">
{selectedTask.stateDate ? format(parseISO(selectedTask.stateDate), 'PPP') : 'Not set'}
</p>
</div>
<div className="space-y-1">
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<CalendarIcon className="h-4 w-4" /> End Date
</h4>
<p className="font-medium">
{selectedTask.endDate ? format(parseISO(selectedTask.endDate), 'PPP') : 'Not set'}
</p>
</div>
<div className="space-y-1">
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<User className="h-4 w-4" /> Assignee
</h4>
<p className="font-medium">
{employees.find(e => e.id === selectedTask.assignedTo)?.firstName} {employees.find(e => e.id === selectedTask.assignedTo)?.lastName || 'Unassigned'}
</p>
</div>
<div className="space-y-1">
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Clock className="h-4 w-4" /> Created At
</h4>
<p className="font-medium">
{selectedTask.createdAt ? format(parseISO(selectedTask.createdAt), 'PPP') : 'Unknown'}
</p>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">Description</h4>
<div className="p-4 rounded-md bg-muted/50 text-sm whitespace-pre-wrap">
{selectedTask.description || 'No description provided.'}
</div>
</div>
<div className="flex justify-end pt-4 border-t">
<Button onClick={() => navigate(`/tasks/${selectedTask.id}`)}>
View Full Details
</Button>
</div>
</div>
</div>
)}
</CustomSheet>
</div>
);
};