|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
"use client";
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
import { useState, useEffect, Fragment } from "react";
|
|
|
|
import { Card, CardHeader, CardContent } from "./ui/Card";
|
|
|
|
import { Card, CardHeader, CardContent } from "./ui/Card";
|
|
|
|
import Button from "./ui/Button";
|
|
|
|
import Button from "./ui/Button";
|
|
|
|
import Badge from "./ui/Badge";
|
|
|
|
import Badge from "./ui/Badge";
|
|
|
|
@@ -35,7 +35,7 @@ export default function ProjectTasksList() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchAllTasks();
|
|
|
|
fetchAllTasks();
|
|
|
|
}, []); // Calculate task status based on date_added and max_wait_days
|
|
|
|
}, []); // Calculate task status based on date_added and max_wait_days
|
|
|
|
const getTaskStatus = (task) => {
|
|
|
|
const getTaskStatus = (task) => {
|
|
|
|
if (task.status === "completed" || task.status === "cancelled") {
|
|
|
|
if (task.status === "completed" || task.status === "cancelled") {
|
|
|
|
return { type: "completed", days: 0 };
|
|
|
|
return { type: "completed", days: 0 };
|
|
|
|
@@ -43,8 +43,12 @@ export default function ProjectTasksList() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// For in-progress tasks, use date_started if available and valid, otherwise fall back to date_added
|
|
|
|
// For in-progress tasks, use date_started if available and valid, otherwise fall back to date_added
|
|
|
|
let referenceDate;
|
|
|
|
let referenceDate;
|
|
|
|
console.log(task.date_started)
|
|
|
|
console.log(task.date_started);
|
|
|
|
if (task.status === "in_progress" && task.date_started && task.date_started.trim() !== "") {
|
|
|
|
if (
|
|
|
|
|
|
|
|
task.status === "in_progress" &&
|
|
|
|
|
|
|
|
task.date_started &&
|
|
|
|
|
|
|
|
task.date_started.trim() !== ""
|
|
|
|
|
|
|
|
) {
|
|
|
|
// Handle the format "2025-06-20 08:40:38"
|
|
|
|
// Handle the format "2025-06-20 08:40:38"
|
|
|
|
referenceDate = new Date(task.date_started);
|
|
|
|
referenceDate = new Date(task.date_started);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@@ -65,9 +69,17 @@ export default function ProjectTasksList() {
|
|
|
|
|
|
|
|
|
|
|
|
if (task.status === "in_progress") {
|
|
|
|
if (task.status === "in_progress") {
|
|
|
|
if (daysRemaining < 0) {
|
|
|
|
if (daysRemaining < 0) {
|
|
|
|
return { type: "overdue", days: Math.abs(daysRemaining), daysRemaining: daysRemaining };
|
|
|
|
return {
|
|
|
|
|
|
|
|
type: "overdue",
|
|
|
|
|
|
|
|
days: Math.abs(daysRemaining),
|
|
|
|
|
|
|
|
daysRemaining: daysRemaining,
|
|
|
|
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
return { type: "in_progress", days: daysRemaining, daysRemaining: daysRemaining };
|
|
|
|
return {
|
|
|
|
|
|
|
|
type: "in_progress",
|
|
|
|
|
|
|
|
days: daysRemaining,
|
|
|
|
|
|
|
|
daysRemaining: daysRemaining,
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -81,7 +93,12 @@ export default function ProjectTasksList() {
|
|
|
|
return { type: "pending", days: maxWaitDays - daysElapsed };
|
|
|
|
return { type: "pending", days: maxWaitDays - daysElapsed };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error parsing date:", task.date_added, task.date_started, error);
|
|
|
|
console.error(
|
|
|
|
|
|
|
|
"Error parsing date:",
|
|
|
|
|
|
|
|
task.date_added,
|
|
|
|
|
|
|
|
task.date_started,
|
|
|
|
|
|
|
|
error
|
|
|
|
|
|
|
|
);
|
|
|
|
return { type: "pending", days: 0 };
|
|
|
|
return { type: "pending", days: 0 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@@ -120,18 +137,25 @@ export default function ProjectTasksList() {
|
|
|
|
// Sort in_progress tasks by time left (urgent first - less time left comes first)
|
|
|
|
// Sort in_progress tasks by time left (urgent first - less time left comes first)
|
|
|
|
groups.in_progress.sort((a, b) => {
|
|
|
|
groups.in_progress.sort((a, b) => {
|
|
|
|
// If both have valid time remaining, sort by days remaining (ascending - urgent first)
|
|
|
|
// If both have valid time remaining, sort by days remaining (ascending - urgent first)
|
|
|
|
if (!isNaN(a.statusInfo.daysRemaining) && !isNaN(b.statusInfo.daysRemaining)) {
|
|
|
|
if (
|
|
|
|
|
|
|
|
!isNaN(a.statusInfo.daysRemaining) &&
|
|
|
|
|
|
|
|
!isNaN(b.statusInfo.daysRemaining)
|
|
|
|
|
|
|
|
) {
|
|
|
|
return a.statusInfo.daysRemaining - b.statusInfo.daysRemaining;
|
|
|
|
return a.statusInfo.daysRemaining - b.statusInfo.daysRemaining;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If one has invalid time, sort by date_started as fallback
|
|
|
|
// If one has invalid time, sort by date_started as fallback
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const dateA = a.date_started ? new Date(a.date_started) : new Date(a.date_added);
|
|
|
|
const dateA = a.date_started
|
|
|
|
const dateB = b.date_started ? new Date(b.date_started) : new Date(b.date_added);
|
|
|
|
? 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
|
|
|
|
return dateA - dateB; // Oldest started first
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}); // Sort completed tasks by date_completed if available, otherwise by date_added (most recently completed first)
|
|
|
|
}); // Sort completed tasks by date_completed if available, otherwise by date_added (most recently completed first)
|
|
|
|
groups.completed.sort((a, b) => {
|
|
|
|
groups.completed.sort((a, b) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Try to use date_completed first
|
|
|
|
// Try to use date_completed first
|
|
|
|
@@ -225,7 +249,8 @@ export default function ProjectTasksList() {
|
|
|
|
if (days > 7) return "danger";
|
|
|
|
if (days > 7) return "danger";
|
|
|
|
if (days > 3) return "warning";
|
|
|
|
if (days > 3) return "warning";
|
|
|
|
return "high";
|
|
|
|
return "high";
|
|
|
|
}; const TaskRow = ({ task, showTimeLeft = false }) => (
|
|
|
|
};
|
|
|
|
|
|
|
|
const TaskRow = ({ task, showTimeLeft = false }) => (
|
|
|
|
<tr className="hover:bg-gray-50 border-b border-gray-200">
|
|
|
|
<tr className="hover:bg-gray-50 border-b border-gray-200">
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
@@ -236,49 +261,58 @@ export default function ProjectTasksList() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
|
|
|
{" "}
|
|
|
|
<Link
|
|
|
|
<Link
|
|
|
|
href={`/projects/${task.project_id}`}
|
|
|
|
href={`/projects/${task.project_id}`}
|
|
|
|
className="text-blue-600 hover:text-blue-800 font-medium"
|
|
|
|
className="text-blue-600 hover:text-blue-800 font-medium"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{task.project_name}
|
|
|
|
{task.project_name}
|
|
|
|
</Link>
|
|
|
|
</Link>
|
|
|
|
</td> <td className="px-4 py-3 text-sm text-gray-600">{task.city || 'N/A'}</td>
|
|
|
|
</td>
|
|
|
|
<td className="px-4 py-3 text-sm text-gray-600">{task.address || 'N/A'}</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 && (
|
|
|
|
{showTimeLeft && (
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
{task.statusInfo && task.statusInfo.type === "in_progress" && (
|
|
|
|
{task.statusInfo && task.statusInfo.type === "in_progress" && (
|
|
|
|
<Badge
|
|
|
|
<Badge
|
|
|
|
variant={task.statusInfo.daysRemaining <= 2 ? "warning" : "secondary"}
|
|
|
|
variant={
|
|
|
|
|
|
|
|
task.statusInfo.daysRemaining <= 2 ? "warning" : "secondary"
|
|
|
|
|
|
|
|
}
|
|
|
|
size="sm"
|
|
|
|
size="sm"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{!isNaN(task.statusInfo.daysRemaining) ? (
|
|
|
|
{!isNaN(task.statusInfo.daysRemaining)
|
|
|
|
task.statusInfo.daysRemaining > 0
|
|
|
|
? task.statusInfo.daysRemaining > 0
|
|
|
|
? `${task.statusInfo.daysRemaining}d left`
|
|
|
|
? `${task.statusInfo.daysRemaining}d left`
|
|
|
|
: `${Math.abs(task.statusInfo.daysRemaining)}d overdue`
|
|
|
|
: `${Math.abs(task.statusInfo.daysRemaining)}d overdue`
|
|
|
|
) : (
|
|
|
|
: "Calculating..."}
|
|
|
|
"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>
|
|
|
|
</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>
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
)} <td className="px-4 py-3 text-sm text-gray-500">
|
|
|
|
)}
|
|
|
|
|
|
|
|
<td className="px-4 py-3 text-sm text-gray-500">
|
|
|
|
{task.status === "completed" && task.date_completed ? (
|
|
|
|
{task.status === "completed" && task.date_completed ? (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
Completed: {(() => {
|
|
|
|
Completed:{" "}
|
|
|
|
|
|
|
|
{(() => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const completedDate = new Date(task.date_completed);
|
|
|
|
const completedDate = new Date(task.date_completed);
|
|
|
|
return formatDistanceToNow(completedDate, { addSuffix: true });
|
|
|
|
return formatDistanceToNow(completedDate, {
|
|
|
|
|
|
|
|
addSuffix: true,
|
|
|
|
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
return task.date_completed;
|
|
|
|
return task.date_completed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -288,7 +322,8 @@ export default function ProjectTasksList() {
|
|
|
|
) : task.status === "in_progress" && task.date_started ? (
|
|
|
|
) : task.status === "in_progress" && task.date_started ? (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
Started: {(() => {
|
|
|
|
Started:{" "}
|
|
|
|
|
|
|
|
{(() => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const startedDate = new Date(task.date_started);
|
|
|
|
const startedDate = new Date(task.date_started);
|
|
|
|
return formatDistanceToNow(startedDate, { addSuffix: true });
|
|
|
|
return formatDistanceToNow(startedDate, { addSuffix: true });
|
|
|
|
@@ -313,7 +348,8 @@ export default function ProjectTasksList() {
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
<td className="px-4 py-3 text-sm text-gray-500">
|
|
|
|
<td className="px-4 py-3 text-sm text-gray-500">
|
|
|
|
{task.max_wait_days} days
|
|
|
|
{task.max_wait_days} days
|
|
|
|
</td> <td className="px-4 py-3">
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
<TaskStatusDropdownSimple
|
|
|
|
<TaskStatusDropdownSimple
|
|
|
|
task={task}
|
|
|
|
task={task}
|
|
|
|
size="sm"
|
|
|
|
size="sm"
|
|
|
|
@@ -321,7 +357,8 @@ export default function ProjectTasksList() {
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</tr>
|
|
|
|
); const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
|
|
|
);
|
|
|
|
|
|
|
|
const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
|
|
|
const filteredTasks = filterTasks(tasks);
|
|
|
|
const filteredTasks = filterTasks(tasks);
|
|
|
|
const groupedTasks = groupTasksByName(filteredTasks);
|
|
|
|
const groupedTasks = groupTasksByName(filteredTasks);
|
|
|
|
const colSpan = showTimeLeft ? "8" : "7";
|
|
|
|
const colSpan = showTimeLeft ? "8" : "7";
|
|
|
|
@@ -333,25 +370,27 @@ export default function ProjectTasksList() {
|
|
|
|
<tr>
|
|
|
|
<tr>
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Task Name
|
|
|
|
Task Name
|
|
|
|
</th>
|
|
|
|
</th>{" "}
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Project
|
|
|
|
Project
|
|
|
|
</th> <th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
</th>
|
|
|
|
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
City
|
|
|
|
City
|
|
|
|
</th>
|
|
|
|
</th>
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Address
|
|
|
|
Address
|
|
|
|
</th>
|
|
|
|
</th>{" "}
|
|
|
|
{showTimeLeft && (
|
|
|
|
{showTimeLeft && (
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Time Left
|
|
|
|
Time Left
|
|
|
|
</th>
|
|
|
|
</th>
|
|
|
|
)} <th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
)}
|
|
|
|
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Date Info
|
|
|
|
Date Info
|
|
|
|
</th>
|
|
|
|
</th>
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Max Wait
|
|
|
|
Max Wait
|
|
|
|
</th>
|
|
|
|
</th>{" "}
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
|
|
|
Actions
|
|
|
|
Actions
|
|
|
|
</th>
|
|
|
|
</th>
|
|
|
|
@@ -359,7 +398,7 @@ export default function ProjectTasksList() {
|
|
|
|
</thead>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
<tbody>
|
|
|
|
{Object.entries(groupedTasks).map(([groupName, groupTasks]) => (
|
|
|
|
{Object.entries(groupedTasks).map(([groupName, groupTasks]) => (
|
|
|
|
<>
|
|
|
|
<Fragment key={`group-fragment-${groupName}`}>
|
|
|
|
{showGrouped && groupName !== "All Tasks" && (
|
|
|
|
{showGrouped && groupName !== "All Tasks" && (
|
|
|
|
<tr key={`group-${groupName}`}>
|
|
|
|
<tr key={`group-${groupName}`}>
|
|
|
|
<td
|
|
|
|
<td
|
|
|
|
@@ -371,9 +410,13 @@ export default function ProjectTasksList() {
|
|
|
|
</tr>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
{groupTasks.map((task) => (
|
|
|
|
{groupTasks.map((task) => (
|
|
|
|
<TaskRow key={task.id} task={task} showTimeLeft={showTimeLeft} />
|
|
|
|
<TaskRow
|
|
|
|
|
|
|
|
key={task.id}
|
|
|
|
|
|
|
|
task={task}
|
|
|
|
|
|
|
|
showTimeLeft={showTimeLeft}
|
|
|
|
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
</Fragment>
|
|
|
|
))}
|
|
|
|
))}
|
|
|
|
</tbody>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</table>
|
|
|
|
@@ -424,7 +467,9 @@ export default function ProjectTasksList() {
|
|
|
|
<div className="text-sm text-gray-600">Completed</div>
|
|
|
|
<div className="text-sm text-gray-600">Completed</div>
|
|
|
|
</CardContent>
|
|
|
|
</CardContent>
|
|
|
|
</Card>
|
|
|
|
</Card>
|
|
|
|
</div> {/* Search and Controls */} <SearchBar
|
|
|
|
</div>{" "}
|
|
|
|
|
|
|
|
{/* Search and Controls */}{" "}
|
|
|
|
|
|
|
|
<SearchBar
|
|
|
|
searchTerm={searchTerm}
|
|
|
|
searchTerm={searchTerm}
|
|
|
|
onSearchChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
onSearchChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
placeholder="Search tasks, projects, city, or address..."
|
|
|
|
placeholder="Search tasks, projects, city, or address..."
|
|
|
|
@@ -437,7 +482,9 @@ export default function ProjectTasksList() {
|
|
|
|
filters={
|
|
|
|
filters={
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<label className="text-sm font-medium text-gray-700">Group by:</label>
|
|
|
|
<label className="text-sm font-medium text-gray-700">
|
|
|
|
|
|
|
|
Group by:
|
|
|
|
|
|
|
|
</label>
|
|
|
|
<Select
|
|
|
|
<Select
|
|
|
|
value={groupBy}
|
|
|
|
value={groupBy}
|
|
|
|
onChange={(e) => setGroupBy(e.target.value)}
|
|
|
|
onChange={(e) => setGroupBy(e.target.value)}
|
|
|
|
@@ -449,7 +496,8 @@ export default function ProjectTasksList() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/> {/* Task Tables */}
|
|
|
|
/>{" "}
|
|
|
|
|
|
|
|
{/* Task Tables */}
|
|
|
|
<div className="space-y-8">
|
|
|
|
<div className="space-y-8">
|
|
|
|
{/* Pending Tasks */}
|
|
|
|
{/* Pending Tasks */}
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
|