feat: Add ProjectTasksPage and ProjectTasksDashboard components with task categorization and filtering
This commit is contained in:
15
src/app/project-tasks/page.js
Normal file
15
src/app/project-tasks/page.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import ProjectTasksDashboard from "@/components/ProjectTasksDashboard";
|
||||||
|
import PageContainer from "@/components/ui/PageContainer";
|
||||||
|
import PageHeader from "@/components/ui/PageHeader";
|
||||||
|
|
||||||
|
export default function ProjectTasksPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader
|
||||||
|
title="Project Tasks"
|
||||||
|
description="Monitor pending tasks, overdue items, and recent activity across all projects"
|
||||||
|
/>
|
||||||
|
<ProjectTasksDashboard />
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
519
src/components/ProjectTasksDashboard.js
Normal file
519
src/components/ProjectTasksDashboard.js
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Card, CardHeader, CardContent } from "./ui/Card";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import Badge from "./ui/Badge";
|
||||||
|
import SearchBar from "./ui/SearchBar";
|
||||||
|
import { Select } from "./ui/Input";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
differenceInCalendarDays,
|
||||||
|
parseISO,
|
||||||
|
formatDistanceToNow,
|
||||||
|
} from "date-fns";
|
||||||
|
|
||||||
|
export default function ProjectTasksDashboard() {
|
||||||
|
const [allTasks, setAllTasks] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filter, setFilter] = useState("all");
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAllTasks = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/all-project-tasks");
|
||||||
|
const tasks = await res.json();
|
||||||
|
setAllTasks(tasks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch project tasks:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAllTasks();
|
||||||
|
}, []);
|
||||||
|
// Calculate task status based on date_added and max_wait_days
|
||||||
|
const getTaskStatus = (task) => {
|
||||||
|
if (task.status === "completed" || task.status === "cancelled") {
|
||||||
|
return { type: "completed", days: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different date formats
|
||||||
|
const addedDate = task.date_added.includes("T")
|
||||||
|
? parseISO(task.date_added)
|
||||||
|
: new Date(task.date_added + "T00:00:00");
|
||||||
|
|
||||||
|
const daysElapsed = differenceInCalendarDays(new Date(), addedDate);
|
||||||
|
const maxWaitDays = task.max_wait_days || 0;
|
||||||
|
const daysOverdue = daysElapsed - maxWaitDays;
|
||||||
|
|
||||||
|
if (daysOverdue > 0) {
|
||||||
|
return { type: "overdue", days: daysOverdue };
|
||||||
|
} else if (maxWaitDays - daysElapsed <= 2) {
|
||||||
|
return { type: "due_soon", days: maxWaitDays - daysElapsed };
|
||||||
|
} else {
|
||||||
|
return { type: "pending", days: maxWaitDays - daysElapsed };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing date:", task.date_added, error);
|
||||||
|
return { type: "pending", days: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Categorize tasks
|
||||||
|
const categorizeTasks = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const categories = {
|
||||||
|
overdue: [],
|
||||||
|
due_soon: [],
|
||||||
|
pending: [],
|
||||||
|
in_progress: [],
|
||||||
|
recent_completed: [],
|
||||||
|
};
|
||||||
|
allTasks.forEach((task) => {
|
||||||
|
const taskStatus = getTaskStatus(task);
|
||||||
|
try {
|
||||||
|
const addedDate = task.date_added.includes("T")
|
||||||
|
? parseISO(task.date_added)
|
||||||
|
: new Date(task.date_added + "T00:00:00");
|
||||||
|
const daysAgo = differenceInCalendarDays(now, addedDate); // First check if task is overdue (regardless of status)
|
||||||
|
if (
|
||||||
|
taskStatus.type === "overdue" &&
|
||||||
|
task.status !== "completed" &&
|
||||||
|
task.status !== "cancelled"
|
||||||
|
) {
|
||||||
|
categories.overdue.push({ ...task, statusInfo: taskStatus });
|
||||||
|
}
|
||||||
|
// Then check if it's due soon (regardless of status)
|
||||||
|
else if (
|
||||||
|
taskStatus.type === "due_soon" &&
|
||||||
|
task.status !== "completed" &&
|
||||||
|
task.status !== "cancelled"
|
||||||
|
) {
|
||||||
|
categories.due_soon.push({ ...task, statusInfo: taskStatus });
|
||||||
|
}
|
||||||
|
// Then categorize by actual status
|
||||||
|
else if (task.status === "pending") {
|
||||||
|
categories.pending.push({ ...task, statusInfo: taskStatus });
|
||||||
|
} else if (task.status === "in_progress") {
|
||||||
|
categories.in_progress.push({ ...task, statusInfo: taskStatus });
|
||||||
|
} else if (task.status === "completed" || task.status === "cancelled") {
|
||||||
|
// Show all completed/cancelled tasks (most recent activity)
|
||||||
|
categories.recent_completed.push({ ...task, statusInfo: taskStatus });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing task:", task, error);
|
||||||
|
// Still add to appropriate category if there's an error
|
||||||
|
if (task.status === "pending") {
|
||||||
|
categories.pending.push({
|
||||||
|
...task,
|
||||||
|
statusInfo: { type: "pending", days: 0 },
|
||||||
|
});
|
||||||
|
} else if (task.status === "in_progress") {
|
||||||
|
categories.in_progress.push({
|
||||||
|
...task,
|
||||||
|
statusInfo: { type: "pending", days: 0 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}); // Sort each category
|
||||||
|
categories.overdue.sort((a, b) => b.statusInfo.days - a.statusInfo.days);
|
||||||
|
categories.due_soon.sort((a, b) => a.statusInfo.days - b.statusInfo.days);
|
||||||
|
categories.pending.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
const dateA = a.date_added.includes("T")
|
||||||
|
? parseISO(a.date_added)
|
||||||
|
: new Date(a.date_added + "T00:00:00");
|
||||||
|
const dateB = b.date_added.includes("T")
|
||||||
|
? parseISO(b.date_added)
|
||||||
|
: new Date(b.date_added + "T00:00:00");
|
||||||
|
return dateB - dateA;
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
categories.in_progress.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
const dateA = a.date_added.includes("T")
|
||||||
|
? parseISO(a.date_added)
|
||||||
|
: new Date(a.date_added + "T00:00:00");
|
||||||
|
const dateB = b.date_added.includes("T")
|
||||||
|
? parseISO(b.date_added)
|
||||||
|
: new Date(b.date_added + "T00:00:00");
|
||||||
|
return dateB - dateA;
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
categories.recent_completed.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
const dateA = a.date_added.includes("T")
|
||||||
|
? parseISO(a.date_added)
|
||||||
|
: new Date(a.date_added + "T00:00:00");
|
||||||
|
const dateB = b.date_added.includes("T")
|
||||||
|
? parseISO(b.date_added)
|
||||||
|
: new Date(b.date_added + "T00:00:00");
|
||||||
|
return dateB - dateA;
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorizedTasks = categorizeTasks();
|
||||||
|
|
||||||
|
// Filter tasks based on search and filter
|
||||||
|
const filterTasks = (tasks) => {
|
||||||
|
if (!searchTerm) return tasks;
|
||||||
|
return tasks.filter(
|
||||||
|
(task) =>
|
||||||
|
task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
task.project_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
task.wp.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const getVisibleTasks = () => {
|
||||||
|
switch (filter) {
|
||||||
|
case "overdue":
|
||||||
|
return filterTasks(categorizedTasks.overdue);
|
||||||
|
case "due_soon":
|
||||||
|
return filterTasks(categorizedTasks.due_soon);
|
||||||
|
case "pending":
|
||||||
|
return filterTasks(categorizedTasks.pending);
|
||||||
|
case "in_progress":
|
||||||
|
return filterTasks(categorizedTasks.in_progress);
|
||||||
|
case "completed":
|
||||||
|
return filterTasks(categorizedTasks.recent_completed);
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
overdue: filterTasks(categorizedTasks.overdue),
|
||||||
|
due_soon: filterTasks(categorizedTasks.due_soon),
|
||||||
|
pending: filterTasks(categorizedTasks.pending),
|
||||||
|
in_progress: filterTasks(categorizedTasks.in_progress),
|
||||||
|
recent_completed: filterTasks(categorizedTasks.recent_completed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleTasks = getVisibleTasks();
|
||||||
|
|
||||||
|
const handleStatusChange = async (taskId, newStatus) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/project-tasks/${taskId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: newStatus }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
// Refresh tasks
|
||||||
|
const res2 = await fetch("/api/all-project-tasks");
|
||||||
|
const tasks = await res2.json();
|
||||||
|
setAllTasks(tasks);
|
||||||
|
} else {
|
||||||
|
alert("Failed to update task status");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("Error updating task status");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityVariant = (priority) => {
|
||||||
|
switch (priority) {
|
||||||
|
case "urgent":
|
||||||
|
return "urgent";
|
||||||
|
case "high":
|
||||||
|
return "high";
|
||||||
|
case "normal":
|
||||||
|
return "normal";
|
||||||
|
case "low":
|
||||||
|
return "low";
|
||||||
|
default:
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadgeVariant = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case "completed":
|
||||||
|
return "success";
|
||||||
|
case "in_progress":
|
||||||
|
return "primary";
|
||||||
|
case "pending":
|
||||||
|
return "warning";
|
||||||
|
case "cancelled":
|
||||||
|
return "danger";
|
||||||
|
default:
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOverdueBadgeVariant = (days) => {
|
||||||
|
if (days > 7) return "danger";
|
||||||
|
if (days > 3) return "warning";
|
||||||
|
return "high";
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskCard = ({ task, showStatusBadge = false }) => (
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow bg-white">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 truncate">
|
||||||
|
{task.task_name}
|
||||||
|
</h4>
|
||||||
|
<Badge variant={getPriorityVariant(task.priority)} size="sm">
|
||||||
|
{task.priority}
|
||||||
|
</Badge>
|
||||||
|
{showStatusBadge && (
|
||||||
|
<Badge variant={getStatusBadgeVariant(task.status)} size="sm">
|
||||||
|
{task.status.replace("_", " ")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-600 mb-2">
|
||||||
|
<Link
|
||||||
|
href={`/projects/${task.project_id}`}
|
||||||
|
className="font-medium text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
{task.project_name}
|
||||||
|
</Link>
|
||||||
|
<span>WP: {task.wp}</span>
|
||||||
|
<span>Plot: {task.plot}</span>
|
||||||
|
</div>{" "}
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||||
|
<span>
|
||||||
|
Added:{" "}
|
||||||
|
{(() => {
|
||||||
|
try {
|
||||||
|
const addedDate = task.date_added.includes("T")
|
||||||
|
? parseISO(task.date_added)
|
||||||
|
: new Date(task.date_added + "T00:00:00");
|
||||||
|
return formatDistanceToNow(addedDate, { addSuffix: true });
|
||||||
|
} catch (error) {
|
||||||
|
return task.date_added;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<span>Max wait: {task.max_wait_days} days</span>
|
||||||
|
<span>Type: {task.task_type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex flex-col items-end gap-2">
|
||||||
|
{task.statusInfo && task.statusInfo.type === "overdue" && (
|
||||||
|
<Badge
|
||||||
|
variant={getOverdueBadgeVariant(task.statusInfo.days)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{task.statusInfo.days} days overdue
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{task.statusInfo && task.statusInfo.type === "due_soon" && (
|
||||||
|
<Badge variant="warning" size="sm">
|
||||||
|
Due in {task.statusInfo.days} days
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{(task.status === "pending" || task.status === "in_progress") && (
|
||||||
|
<select
|
||||||
|
value={task.status}
|
||||||
|
onChange={(e) => handleStatusChange(task.id, e.target.value)}
|
||||||
|
className="px-2 py-1 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="in_progress">In Progress</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SectionCard = ({ title, tasks, variant = "default", count }) => (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">{title}</h3>
|
||||||
|
<Badge variant={variant} size="md">
|
||||||
|
{count || tasks.length} {tasks.length === 1 ? "task" : "tasks"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{tasks.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p className="text-sm">No tasks in this category</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tasks.map((task) => (
|
||||||
|
<TaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
showStatusBadge={
|
||||||
|
title === "📊 Recent Activity" || title === "🔄 In Progress"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
const filterOptions = [
|
||||||
|
{ value: "all", label: "All Categories" },
|
||||||
|
{ value: "overdue", label: "Overdue" },
|
||||||
|
{ value: "due_soon", label: "Due Soon" },
|
||||||
|
{ value: "pending", label: "Pending" },
|
||||||
|
{ value: "in_progress", label: "In Progress" },
|
||||||
|
{ value: "completed", label: "Recent Activity" },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="animate-pulse space-y-4">
|
||||||
|
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div key={i} className="h-64 bg-gray-200 rounded-lg"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{" "}
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-5 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{categorizedTasks.overdue.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">Overdue</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">
|
||||||
|
{categorizedTasks.due_soon.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">Due Soon</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{categorizedTasks.pending.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">Pending</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">
|
||||||
|
{categorizedTasks.in_progress.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">In Progress</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{categorizedTasks.recent_completed.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">Recent Activity</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<SearchBar
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearchChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search tasks, projects, or WP..."
|
||||||
|
resultsCount={
|
||||||
|
filter === "all"
|
||||||
|
? allTasks.length
|
||||||
|
: Array.isArray(visibleTasks)
|
||||||
|
? visibleTasks.length
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
resultsText="tasks"
|
||||||
|
filters={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">View:</label>
|
||||||
|
<Select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="min-w-[150px]"
|
||||||
|
>
|
||||||
|
{filterOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>{" "}
|
||||||
|
{/* Task Sections */}
|
||||||
|
{filter === "all" ? (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
<SectionCard
|
||||||
|
title="🚨 Overdue Tasks"
|
||||||
|
tasks={visibleTasks.overdue}
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
<SectionCard
|
||||||
|
title="⚠️ Due Soon"
|
||||||
|
tasks={visibleTasks.due_soon}
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<SectionCard
|
||||||
|
title="📋 Pending Tasks"
|
||||||
|
tasks={visibleTasks.pending}
|
||||||
|
variant="primary"
|
||||||
|
/>
|
||||||
|
<SectionCard
|
||||||
|
title="🔄 In Progress"
|
||||||
|
tasks={visibleTasks.in_progress}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
<SectionCard
|
||||||
|
title="📊 Recent Activity"
|
||||||
|
tasks={visibleTasks.recent_completed}
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<SectionCard
|
||||||
|
title={
|
||||||
|
filterOptions.find((f) => f.value === filter)?.label || "Tasks"
|
||||||
|
}
|
||||||
|
tasks={Array.isArray(visibleTasks) ? visibleTasks : []}
|
||||||
|
variant={
|
||||||
|
filter === "overdue"
|
||||||
|
? "danger"
|
||||||
|
: filter === "due_soon"
|
||||||
|
? "warning"
|
||||||
|
: filter === "pending"
|
||||||
|
? "primary"
|
||||||
|
: filter === "in_progress"
|
||||||
|
? "secondary"
|
||||||
|
: "success"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ const Navigation = () => {
|
|||||||
{ href: "/", label: "Dashboard" },
|
{ href: "/", label: "Dashboard" },
|
||||||
{ href: "/projects", label: "Projects" },
|
{ href: "/projects", label: "Projects" },
|
||||||
{ href: "/tasks/templates", label: "Task Templates" },
|
{ href: "/tasks/templates", label: "Task Templates" },
|
||||||
{ href: "/tasks", label: "Project Tasks" },
|
{ href: "/project-tasks", label: "Project Tasks" },
|
||||||
{ href: "/contracts", label: "Contracts" },
|
{ href: "/contracts", label: "Contracts" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user