feat: implement ProjectTasksList component and update ProjectTasksPage to use it
feat: add date_completed column to project_tasks table in database initialization feat: update project task status to set date_completed when marking as completed
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import ProjectTasksDashboard from "@/components/ProjectTasksDashboard";
|
import ProjectTasksList from "@/components/ProjectTasksList";
|
||||||
import PageContainer from "@/components/ui/PageContainer";
|
import PageContainer from "@/components/ui/PageContainer";
|
||||||
import PageHeader from "@/components/ui/PageHeader";
|
import PageHeader from "@/components/ui/PageHeader";
|
||||||
|
|
||||||
@@ -7,9 +7,9 @@ export default function ProjectTasksPage() {
|
|||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Project Tasks"
|
title="Project Tasks"
|
||||||
description="Monitor pending tasks, overdue items, and recent activity across all projects"
|
description="View and manage tasks across all projects in a structured list format"
|
||||||
/>
|
/>
|
||||||
<ProjectTasksDashboard />
|
<ProjectTasksList />
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
518
src/components/ProjectTasksList.js
Normal file
518
src/components/ProjectTasksList.js
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
"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 TaskStatusDropdownSimple from "./TaskStatusDropdownSimple";
|
||||||
|
import SearchBar from "./ui/SearchBar";
|
||||||
|
import { Select } from "./ui/Input";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
differenceInCalendarDays,
|
||||||
|
parseISO,
|
||||||
|
formatDistanceToNow,
|
||||||
|
} from "date-fns";
|
||||||
|
import { formatDate } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function ProjectTasksList() {
|
||||||
|
const [allTasks, setAllTasks] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [groupBy, setGroupBy] = useState("none");
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// For in-progress tasks, use date_started if available and valid, otherwise fall back to date_added
|
||||||
|
let referenceDate;
|
||||||
|
console.log(task.date_started)
|
||||||
|
if (task.status === "in_progress" && task.date_started && task.date_started.trim() !== "") {
|
||||||
|
// Handle the format "2025-06-20 08:40:38"
|
||||||
|
referenceDate = new Date(task.date_started);
|
||||||
|
} else {
|
||||||
|
// Handle date_added format
|
||||||
|
referenceDate = task.date_added.includes("T")
|
||||||
|
? parseISO(task.date_added)
|
||||||
|
: new Date(task.date_added);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is valid
|
||||||
|
if (isNaN(referenceDate.getTime())) {
|
||||||
|
throw new Error("Invalid date");
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysElapsed = differenceInCalendarDays(new Date(), referenceDate);
|
||||||
|
const maxWaitDays = task.max_wait_days || 0;
|
||||||
|
const daysRemaining = maxWaitDays - daysElapsed;
|
||||||
|
|
||||||
|
if (task.status === "in_progress") {
|
||||||
|
if (daysRemaining < 0) {
|
||||||
|
return { type: "overdue", days: Math.abs(daysRemaining), daysRemaining: daysRemaining };
|
||||||
|
} else {
|
||||||
|
return { type: "in_progress", days: daysRemaining, daysRemaining: daysRemaining };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For pending tasks, use original logic
|
||||||
|
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, task.date_started, error);
|
||||||
|
return { type: "pending", days: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Group tasks by status
|
||||||
|
const groupTasksByStatus = () => {
|
||||||
|
const groups = {
|
||||||
|
pending: [],
|
||||||
|
in_progress: [],
|
||||||
|
completed: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
allTasks.forEach((task) => {
|
||||||
|
const statusInfo = getTaskStatus(task);
|
||||||
|
const taskWithStatus = { ...task, statusInfo };
|
||||||
|
|
||||||
|
if (task.status === "completed" || task.status === "cancelled") {
|
||||||
|
groups.completed.push(taskWithStatus);
|
||||||
|
} else if (task.status === "in_progress") {
|
||||||
|
groups.in_progress.push(taskWithStatus);
|
||||||
|
} else {
|
||||||
|
groups.pending.push(taskWithStatus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort pending tasks by date_added (newest first)
|
||||||
|
groups.pending.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
const dateA = new Date(a.date_added);
|
||||||
|
const dateB = new Date(b.date_added);
|
||||||
|
return dateB - dateA; // Newest first
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort in_progress tasks by time left (urgent first - less time left comes first)
|
||||||
|
groups.in_progress.sort((a, b) => {
|
||||||
|
// If both have valid time remaining, sort by days remaining (ascending - urgent first)
|
||||||
|
if (!isNaN(a.statusInfo.daysRemaining) && !isNaN(b.statusInfo.daysRemaining)) {
|
||||||
|
return a.statusInfo.daysRemaining - b.statusInfo.daysRemaining;
|
||||||
|
}
|
||||||
|
// If one has invalid time, sort by date_started as fallback
|
||||||
|
try {
|
||||||
|
const dateA = a.date_started ? new Date(a.date_started) : new Date(a.date_added);
|
||||||
|
const dateB = b.date_started ? new Date(b.date_started) : new Date(b.date_added);
|
||||||
|
return dateA - dateB; // Oldest started first
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}); // Sort completed tasks by date_completed if available, otherwise by date_added (most recently completed first)
|
||||||
|
groups.completed.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
// Try to use date_completed first
|
||||||
|
if (a.date_completed && b.date_completed) {
|
||||||
|
const dateA = new Date(a.date_completed);
|
||||||
|
const dateB = new Date(b.date_completed);
|
||||||
|
return dateB - dateA; // Most recently completed first
|
||||||
|
}
|
||||||
|
// If only one has date_completed, prioritize it
|
||||||
|
if (a.date_completed && !b.date_completed) return -1;
|
||||||
|
if (!a.date_completed && b.date_completed) return 1;
|
||||||
|
|
||||||
|
// Fall back to date_added for both
|
||||||
|
const dateA = new Date(a.date_added);
|
||||||
|
const dateB = new Date(b.date_added);
|
||||||
|
return dateB - dateA; // Newest first
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskGroups = groupTasksByStatus();
|
||||||
|
// Filter tasks based on search term
|
||||||
|
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.city?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
task.address?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group tasks by task name when groupBy is set to "task_name"
|
||||||
|
const groupTasksByName = (tasks) => {
|
||||||
|
if (groupBy !== "task_name") return { "All Tasks": tasks };
|
||||||
|
|
||||||
|
const groups = {};
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
const taskName = task.task_name;
|
||||||
|
if (!groups[taskName]) {
|
||||||
|
groups[taskName] = [];
|
||||||
|
}
|
||||||
|
groups[taskName].push(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 getOverdueBadgeVariant = (days) => {
|
||||||
|
if (days > 7) return "danger";
|
||||||
|
if (days > 3) return "warning";
|
||||||
|
return "high";
|
||||||
|
}; const TaskRow = ({ task, showTimeLeft = false }) => (
|
||||||
|
<tr className="hover:bg-gray-50 border-b border-gray-200">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900">{task.task_name}</span>
|
||||||
|
<Badge variant={getPriorityVariant(task.priority)} size="sm">
|
||||||
|
{task.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<Link
|
||||||
|
href={`/projects/${task.project_id}`}
|
||||||
|
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||||
|
>
|
||||||
|
{task.project_name}
|
||||||
|
</Link>
|
||||||
|
</td> <td className="px-4 py-3 text-sm text-gray-600">{task.city || 'N/A'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-gray-600">{task.address || 'N/A'}</td>
|
||||||
|
{showTimeLeft && (
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{task.statusInfo && task.statusInfo.type === "in_progress" && (
|
||||||
|
<Badge
|
||||||
|
variant={task.statusInfo.daysRemaining <= 2 ? "warning" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{!isNaN(task.statusInfo.daysRemaining) ? (
|
||||||
|
task.statusInfo.daysRemaining > 0
|
||||||
|
? `${task.statusInfo.daysRemaining}d left`
|
||||||
|
: `${Math.abs(task.statusInfo.daysRemaining)}d overdue`
|
||||||
|
) : (
|
||||||
|
"Calculating..."
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{task.statusInfo && task.statusInfo.type === "overdue" && task.status === "in_progress" && (
|
||||||
|
<Badge
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{!isNaN(task.statusInfo.daysRemaining) ? `${Math.abs(task.statusInfo.daysRemaining)}d overdue` : "Overdue"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)} <td className="px-4 py-3 text-sm text-gray-500">
|
||||||
|
{task.status === "completed" && task.date_completed ? (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Completed: {(() => {
|
||||||
|
try {
|
||||||
|
const completedDate = new Date(task.date_completed);
|
||||||
|
return formatDistanceToNow(completedDate, { addSuffix: true });
|
||||||
|
} catch (error) {
|
||||||
|
return task.date_completed;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : task.status === "in_progress" && task.date_started ? (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Started: {(() => {
|
||||||
|
try {
|
||||||
|
const startedDate = new Date(task.date_started);
|
||||||
|
return formatDistanceToNow(startedDate, { addSuffix: true });
|
||||||
|
} catch (error) {
|
||||||
|
return task.date_started;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const addedDate = task.date_added.includes("T")
|
||||||
|
? parseISO(task.date_added)
|
||||||
|
: new Date(task.date_added);
|
||||||
|
return formatDistanceToNow(addedDate, { addSuffix: true });
|
||||||
|
} catch (error) {
|
||||||
|
return task.date_added;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-gray-500">
|
||||||
|
{task.max_wait_days} days
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<TaskStatusDropdownSimple
|
||||||
|
task={task}
|
||||||
|
size="sm"
|
||||||
|
onStatusChange={handleStatusChange}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
||||||
|
const filteredTasks = filterTasks(tasks);
|
||||||
|
const groupedTasks = groupTasksByName(filteredTasks);
|
||||||
|
const colSpan = showTimeLeft ? "8" : "7";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full bg-white rounded-lg shadow-sm">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Task Name
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Project
|
||||||
|
</th> <th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
City
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Address
|
||||||
|
</th>
|
||||||
|
{showTimeLeft && (
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Time Left
|
||||||
|
</th>
|
||||||
|
)} <th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Date Info
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Max Wait
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(groupedTasks).map(([groupName, groupTasks]) => (
|
||||||
|
<>
|
||||||
|
{showGrouped && groupName !== "All Tasks" && (
|
||||||
|
<tr key={`group-${groupName}`}>
|
||||||
|
<td
|
||||||
|
colSpan={colSpan}
|
||||||
|
className="px-4 py-2 bg-gray-100 font-medium text-gray-800 text-sm"
|
||||||
|
>
|
||||||
|
{groupName} ({groupTasks.length} tasks)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{groupTasks.map((task) => (
|
||||||
|
<TaskRow key={task.id} task={task} showTimeLeft={showTimeLeft} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{filteredTasks.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>No tasks found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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="h-64 bg-gray-200 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{taskGroups.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">
|
||||||
|
{taskGroups.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">
|
||||||
|
{taskGroups.completed.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">Completed</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div> {/* Search and Controls */} <SearchBar
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearchChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search tasks, projects, city, or address..."
|
||||||
|
resultsCount={
|
||||||
|
filterTasks(taskGroups.pending).length +
|
||||||
|
filterTasks(taskGroups.in_progress).length +
|
||||||
|
filterTasks(taskGroups.completed).length
|
||||||
|
}
|
||||||
|
resultsText="tasks"
|
||||||
|
filters={
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">Group by:</label>
|
||||||
|
<Select
|
||||||
|
value={groupBy}
|
||||||
|
onChange={(e) => setGroupBy(e.target.value)}
|
||||||
|
className="min-w-[120px]"
|
||||||
|
>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="task_name">Task Name</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/> {/* Task Tables */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Pending Tasks */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
Pending Tasks
|
||||||
|
<Badge variant="primary" size="md">
|
||||||
|
{taskGroups.pending.length}
|
||||||
|
</Badge>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Tasks waiting to be started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TaskTable
|
||||||
|
tasks={taskGroups.pending}
|
||||||
|
showGrouped={groupBy === "task_name"}
|
||||||
|
showTimeLeft={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* In Progress Tasks */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
In Progress Tasks
|
||||||
|
<Badge variant="secondary" size="md">
|
||||||
|
{taskGroups.in_progress.length}
|
||||||
|
</Badge>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Tasks currently being worked on - showing time left for completion
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TaskTable
|
||||||
|
tasks={taskGroups.in_progress}
|
||||||
|
showGrouped={groupBy === "task_name"}
|
||||||
|
showTimeLeft={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completed Tasks */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
Completed Tasks
|
||||||
|
<Badge variant="success" size="md">
|
||||||
|
{taskGroups.completed.length}
|
||||||
|
</Badge>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Recently completed and cancelled tasks
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TaskTable
|
||||||
|
tasks={taskGroups.completed}
|
||||||
|
showGrouped={groupBy === "task_name"}
|
||||||
|
showTimeLeft={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -145,7 +145,6 @@ export default function initializeDatabase() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Column already exists, ignore error
|
// Column already exists, ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration: Add is_system column to notes table
|
// Migration: Add is_system column to notes table
|
||||||
try {
|
try {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
@@ -154,4 +153,13 @@ export default function initializeDatabase() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Column already exists, ignore error
|
// Column already exists, ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: Add date_completed column to project_tasks table
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE project_tasks ADD COLUMN date_completed TEXT;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export function getAllProjectTasks() {
|
|||||||
p.project_name,
|
p.project_name,
|
||||||
p.wp,
|
p.wp,
|
||||||
p.plot,
|
p.plot,
|
||||||
|
p.city,
|
||||||
|
p.address,
|
||||||
p.finish_date
|
p.finish_date
|
||||||
FROM project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
@@ -138,8 +140,16 @@ export function updateProjectTaskStatus(taskId, status) {
|
|||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
result = stmt.run(status, taskId);
|
result = stmt.run(status, taskId);
|
||||||
|
} else if (status === "completed") {
|
||||||
|
// Completing a task - set date_completed
|
||||||
|
stmt = db.prepare(`
|
||||||
|
UPDATE project_tasks
|
||||||
|
SET status = ?, date_completed = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
result = stmt.run(status, taskId);
|
||||||
} else {
|
} else {
|
||||||
// Just updating status without changing date_started
|
// Just updating status without changing timestamps
|
||||||
stmt = db.prepare(`
|
stmt = db.prepare(`
|
||||||
UPDATE project_tasks
|
UPDATE project_tasks
|
||||||
SET status = ?
|
SET status = ?
|
||||||
|
|||||||
Reference in New Issue
Block a user