Fix project issues with areas and active filter

This commit is contained in:
Chris Veleris 2024-12-16 17:44:40 +02:00
parent 768e5cbcf8
commit 42aa10b378
7 changed files with 372 additions and 194 deletions

View file

@ -12,7 +12,16 @@ import ConfirmDialog from "../Shared/ConfirmDialog";
import { useDataContext } from "../../contexts/DataContext";
import NewTask from "../Task/NewTask";
import { Project } from "../../entities/Project";
import { Task } from "../../entities/Task";
import { PriorityType, Task } from "../../entities/Task";
type PriorityStyles = Record<PriorityType, string> & { default: string };
const priorityStyles: PriorityStyles = {
high: 'bg-red-500',
medium: 'bg-yellow-500',
low: 'bg-green-500',
default: 'bg-gray-400',
};
const ProjectDetails: React.FC = () => {
const { updateTask, deleteTask, updateProject, deleteProject } = useDataContext();
@ -21,7 +30,7 @@ const ProjectDetails: React.FC = () => {
const { areas } = useDataContext();
const [project, setProject] = useState<Project>();
const [project, setProject] = useState<Project | undefined>(undefined);
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -56,14 +65,15 @@ const ProjectDetails: React.FC = () => {
fetchProject();
}, [id]);
const handleTaskCreate = async (taskData: Partial<Task>) => {
if (!project?.id) {
console.error("Project ID is missing");
const handleTaskCreate = async (taskName: string) => {
if (!project || project.id === undefined) {
console.error("Cannot create task: Project or Project ID is missing");
return;
}
const taskPayload = {
...taskData,
name: taskName,
status: "not_started",
project_id: project.id,
};
@ -124,10 +134,13 @@ const ProjectDetails: React.FC = () => {
};
const handleSaveProject = async (updatedProject: Project) => {
if (!updatedProject) return;
if (!updatedProject || updatedProject.id === undefined) {
console.error("Cannot save project: Project or Project ID is missing");
return;
}
try {
const savedProject = await updateProject(updatedProject.id!, updatedProject);
const savedProject = await updateProject(updatedProject.id, updatedProject);
setProject(savedProject);
setIsModalOpen(false);
} catch (err) {
@ -136,10 +149,13 @@ const ProjectDetails: React.FC = () => {
};
const handleDeleteProject = async () => {
if (!project) return;
if (!project || project.id === undefined) {
console.error("Cannot delete project: Project or Project ID is missing");
return;
}
try {
await deleteProject(project.id!);
await deleteProject(project.id);
navigate("/projects");
} catch (err) {
console.error("Error deleting project:", err);
@ -164,6 +180,14 @@ const ProjectDetails: React.FC = () => {
);
}
if (!project) {
return (
<div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
<div className="text-red-500 text-lg">Project not found.</div>
</div>
);
}
const activeTasks = tasks.filter(task => task.status !== 'done');
const completedTasks = tasks.filter(task => task.status === 'done');
@ -177,12 +201,21 @@ const ProjectDetails: React.FC = () => {
{/* Project Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center">
<FolderIcon className="h-6 w-6 text-gray-500 mr-2" />
<h2 className="text-2xl font-light text-gray-900 dark:text-gray-100">
<FolderIcon className="h-6 w-6 text-gray-500 mr-2" />
<h2 className="text-2xl font-light text-gray-900 dark:text-gray-100 mr-2">
{projectTitle}
</h2>
{/* Priority Circle placed after the title */}
{project.priority && (
<div
className={`w-4 h-4 rounded-full border-2 border-white dark:border-gray-800 ${priorityStyles[project.priority] || priorityStyles.default}`}
title={`Priority: ${priorityLabel(project.priority)}`}
aria-label={`Priority: ${priorityLabel(project.priority)}`}
></div>
)}
</div>
<div className="flex space-x-2">
{/* Edit Project Button */}
<button
onClick={handleEditProject}
className="text-gray-500 hover:text-blue-700 dark:hover:text-blue-300 focus:outline-none"
@ -190,6 +223,7 @@ const ProjectDetails: React.FC = () => {
<PencilSquareIcon className="h-5 w-5" />
</button>
{/* Delete Project Button */}
<button
onClick={() => setIsConfirmDialogOpen(true)}
className="text-gray-500 hover:text-red-700 dark:hover:text-red-300 focus:outline-none"
@ -200,11 +234,11 @@ const ProjectDetails: React.FC = () => {
</div>
{/* Project Area */}
{project?.area && (
{project.area && (
<div className="flex items-center mb-4">
<Squares2X2Icon className="h-5 w-5 text-gray-500 dark:text-gray-400 mr-2" />
<Link
to={`/projects/?area_id=${project?.area.id}`}
to={`/projects/?area_id=${project.area.id}`}
className="text-gray-600 dark:text-gray-400 hover:underline"
>
{project.area.name.toUpperCase()}
@ -213,7 +247,7 @@ const ProjectDetails: React.FC = () => {
)}
{/* Project Description */}
{project?.description && (
{project.description && (
<p className="text-gray-700 dark:text-gray-300 mb-6">
{project.description}
</p>
@ -221,13 +255,7 @@ const ProjectDetails: React.FC = () => {
{/* New Task Form */}
<NewTask
onTaskCreate={(taskName: string) =>
handleTaskCreate({
name: taskName,
status: "not_started",
project_id: project?.id,
})
}
onTaskCreate={handleTaskCreate}
/>
{/* Active Tasks */}
@ -250,7 +278,7 @@ const ProjectDetails: React.FC = () => {
onClick={toggleCompleted}
className="flex items-center justify-between w-full px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-md focus:outline-none"
>
<span className="text-xl font-semibold">Completed Tasks</span>
<span className="text-sm uppercase font-medium">Completed Tasks</span>
<svg
className={`w-6 h-6 transform transition-transform duration-200 ${
isCompletedOpen ? "rotate-180" : "rotate-0"
@ -291,14 +319,15 @@ const ProjectDetails: React.FC = () => {
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleSaveProject}
project={project || undefined}
project={project}
areas={areas}
/>
{/* Confirm Delete Dialog */}
{isConfirmDialogOpen && (
<ConfirmDialog
title="Delete Project"
message={`Are you sure you want to delete the project "${project?.name}"?`}
message={`Are you sure you want to delete the project "${project.name}"?`}
onConfirm={handleDeleteProject}
onCancel={() => setIsConfirmDialogOpen(false)}
/>
@ -308,4 +337,17 @@ const ProjectDetails: React.FC = () => {
);
};
const priorityLabel = (priority: PriorityType) => {
switch (priority) {
case 'high':
return 'High';
case 'medium':
return 'Medium';
case 'low':
return 'Low';
default:
return '';
}
};
export default ProjectDetails;

View file

@ -0,0 +1,137 @@
import React from "react";
import { Link } from "react-router-dom";
import { EllipsisVerticalIcon } from "@heroicons/react/24/solid";
import { Project } from "../../entities/Project";
interface ProjectItemProps {
project: Project;
viewMode: "cards" | "list";
color: string;
getCompletionPercentage: (projectId: number | undefined) => number;
activeDropdown: number | null;
setActiveDropdown: React.Dispatch<React.SetStateAction<number | null>>;
handleEditProject: (project: Project) => void;
setProjectToDelete: React.Dispatch<React.SetStateAction<Project | null>>;
setIsConfirmDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const getProjectInitials = (name: string) => {
const words = name
.trim()
.split(" ")
.filter((word) => word.length > 0);
if (words.length === 1) {
return name.toUpperCase();
}
return words.map((word) => word[0].toUpperCase()).join("");
};
const ProjectItem: React.FC<ProjectItemProps> = ({
project,
viewMode,
color,
getCompletionPercentage,
activeDropdown,
setActiveDropdown,
handleEditProject,
setProjectToDelete,
setIsConfirmDialogOpen,
}) => {
return (
<div
className={`${
viewMode === "cards"
? "bg-gray-50 dark:bg-gray-900 rounded-lg shadow-md relative flex flex-col"
: "bg-gray-50 dark:bg-gray-900 rounded-lg shadow-md relative flex flex-row items-center p-4"
}`}
style={{
minHeight: viewMode === "cards" ? "250px" : "auto",
maxHeight: viewMode === "cards" ? "250px" : "auto",
}}
>
{viewMode === "cards" && (
<div
className="bg-gray-200 dark:bg-gray-700 flex items-center justify-center overflow-hidden rounded-t-lg"
style={{ height: "140px" }}
>
<span className="text-2xl font-extrabold text-gray-500 dark:text-gray-400 opacity-20">
{getProjectInitials(project.name)}
</span>
<div
className={`absolute top-2 left-2 w-3 h-3 rounded-full ${color}`}
></div>
</div>
)}
<div
className={`flex justify-between items-start ${
viewMode === "cards" ? "p-4 flex-1" : "flex-1"
}`}
>
<Link
to={`/project/${project.id}`}
className={`${
viewMode === "cards"
? "text-lg font-semibold text-gray-900 dark:text-gray-100 hover:underline line-clamp-2"
: "text-md font-semibold text-gray-900 dark:text-gray-100 hover:underline"
}`}
>
{project.name}
</Link>
<div className="relative">
<button
className="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-400 focus:outline-none"
onClick={() =>
setActiveDropdown(
activeDropdown === project.id ? null : project.id ?? null
)
}
>
<EllipsisVerticalIcon className="h-5 w-5" />
</button>
{activeDropdown === project.id && (
<div className="absolute right-0 mt-2 w-28 bg-white dark:bg-gray-700 shadow-lg rounded-md z-10">
<button
onClick={() => handleEditProject(project)}
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left"
>
Edit
</button>
<button
onClick={() => {
setProjectToDelete(project);
setIsConfirmDialogOpen(true);
setActiveDropdown(null);
}}
className="block px-4 py-2 text-sm text-red-500 dark:text-red-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left"
>
Delete
</button>
</div>
)}
</div>
</div>
{viewMode === "cards" && (
<div className="absolute bottom-4 left-0 right-0 px-4">
<div className="flex items-center space-x-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
style={{
width: `${getCompletionPercentage(project?.id)}%`,
}}
></div>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400">
{getCompletionPercentage(project?.id)}%
</span>
</div>
</div>
)}
</div>
);
};
export default ProjectItem;

View file

@ -1,43 +1,46 @@
import React, { useState, useEffect } from "react";
import { Project } from "../entities/Project";
import { Link, useSearchParams } from "react-router-dom";
import {
EllipsisVerticalIcon,
MagnifyingGlassIcon,
FolderIcon
FolderIcon,
Squares2X2Icon,
Bars3Icon,
} from "@heroicons/react/24/solid";
import ConfirmDialog from "./Shared/ConfirmDialog";
import ProjectModal from "./Project/ProjectModal";
import { useDataContext } from "../contexts/DataContext";
import useFetchProjects from "../hooks/useFetchProjects";
import { PriorityType, StatusType } from "../entities/Task";
import { useSearchParams } from "react-router-dom";
import ProjectItem from "./Project/ProjectItem";
const getProjectInitials = (name: string) => {
const words = name
.trim()
.split(" ")
.filter((word) => word.length > 0);
if (words.length === 1) {
return name.toUpperCase();
type ProjectTaskCounts = Record<StatusType, number>;
const getPriorityStyles = (priority: PriorityType) => {
switch (priority) {
case "low":
return { color: "bg-green-500" };
case "medium":
return { color: "bg-yellow-500" };
case "high":
return { color: "bg-red-500" };
default:
return { color: "bg-gray-500" };
}
return words.map((word) => word[0].toUpperCase()).join("");
};
const Projects: React.FC = () => {
const { areas, createProject, updateProject, deleteProject } =
useDataContext();
const [taskStatusCounts, setTaskStatusCounts] = useState<Record<number, any>>(
{}
);
const { areas, createProject, updateProject, deleteProject } = useDataContext();
const [taskStatusCounts, setTaskStatusCounts] = useState<Record<number, ProjectTaskCounts>>({});
const [isProjectModalOpen, setIsProjectModalOpen] = useState<boolean>(false);
const [projectToEdit, setProjectToEdit] = useState<Project | null>(null);
const [projectToDelete, setProjectToDelete] = useState<Project | null>(null);
const [isConfirmDialogOpen, setIsConfirmDialogOpen] =
useState<boolean>(false);
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState<boolean>(false);
const [activeDropdown, setActiveDropdown] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState<string>("");
const [viewMode, setViewMode] = useState<"cards" | "list">("cards");
const [searchParams, setSearchParams] = useSearchParams();
const activeFilter = searchParams.get("active") || "all";
const areaFilter = searchParams.get("area_id") || "";
@ -55,14 +58,14 @@ const Projects: React.FC = () => {
const getCompletionPercentage = (projectId: number | undefined) => {
if (!projectId) return 0;
const taskStatus = taskStatusCounts[projectId] || {};
const totalTasks =
(taskStatus.done || 0) +
(taskStatus.not_started || 0) +
(taskStatus.in_progress || 0);
const taskStatus = taskStatusCounts[projectId] || {
not_started: 0,
in_progress: 0,
done: 0,
archived: 0,
};
const totalTasks = taskStatus.done + taskStatus.not_started + taskStatus.in_progress;
if (totalTasks === 0) return 0;
return Math.round((taskStatus.done / totalTasks) * 100);
};
@ -89,9 +92,7 @@ const Projects: React.FC = () => {
mutate();
};
const handleActiveFilterChange = (
e: React.ChangeEvent<HTMLSelectElement>
) => {
const handleActiveFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newActiveFilter = e.target.value;
const params = new URLSearchParams(searchParams);
@ -100,7 +101,6 @@ const Projects: React.FC = () => {
} else {
params.set("active", newActiveFilter);
}
setSearchParams(params);
};
@ -159,51 +159,79 @@ const Projects: React.FC = () => {
</h2>
</div>
{/* Filters for Active Status and Area */}
<div className="flex flex-col md:flex-row md:items-center md:space-x-4 mb-6">
<div className="mb-4 md:mb-0 w-full md:w-1/3">
<label
htmlFor="activeFilter"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
{/* View Mode and Filters */}
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 space-y-4 md:space-y-0">
<div className="flex items-center space-x-2">
<button
onClick={() => setViewMode("cards")}
className={`p-2 rounded-md focus:outline-none ${
viewMode === "cards"
? "bg-blue-500 text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
aria-label="Card View"
>
Status
</label>
<select
id="activeFilter"
value={activeFilter}
onChange={handleActiveFilterChange}
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
<Squares2X2Icon className="h-5 w-5" />
</button>
<button
onClick={() => setViewMode("list")}
className={`p-2 rounded-md focus:outline-none ${
viewMode === "list"
? "bg-blue-500 text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
aria-label="List View"
>
<option value="true">Active</option>
<option value="false">Inactive</option>
<option value="all">All</option>
</select>
<Bars3Icon className="h-5 w-5" />
</button>
</div>
<div className="w-full md:w-1/3">
<label
htmlFor="areaFilter"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Area
</label>
<select
id="areaFilter"
value={areaFilter}
onChange={handleAreaFilterChange}
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Areas</option>
{areas.map((area) => (
<option key={area.id} value={area.id.toString()}>
{area.name}
</option>
))}
</select>
<div className="flex flex-col md:flex-row md:items-center md:space-x-4">
<div className="w-full md:w-auto">
<label
htmlFor="activeFilter"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Status
</label>
<select
id="activeFilter"
value={activeFilter}
onChange={handleActiveFilterChange}
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="true">Active</option>
<option value="false">Inactive</option>
<option value="all">All</option>
</select>
</div>
<div className="w-full md:w-auto">
<label
htmlFor="areaFilter"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Area
</label>
<select
id="areaFilter"
value={areaFilter}
onChange={handleAreaFilterChange}
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Areas</option>
{areas.map((area) => (
<option key={area.id} value={area.id.toString()}>
{area.name}
</option>
))}
</select>
</div>
</div>
</div>
{/* Search Bar with Icon */}
{/* Search Bar */}
<div className="mb-4">
<div className="flex items-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm p-2">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-500 dark:text-gray-400 mr-2" />
@ -217,8 +245,14 @@ const Projects: React.FC = () => {
</div>
</div>
{/* Project Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Projects Grid/List */}
<div
className={`${
viewMode === "cards"
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
: "flex flex-col space-y-4"
}`}
>
{Object.keys(groupedProjects).length === 0 ? (
<div className="text-gray-700 dark:text-gray-300">
No projects found.
@ -226,93 +260,34 @@ const Projects: React.FC = () => {
) : (
Object.keys(groupedProjects).map((areaName) => (
<React.Fragment key={areaName}>
<h3 className="col-span-full text-md uppercase font-light text-gray-800 dark:text-gray-200 mb-2 mt-6">
{areaName}
</h3>
{groupedProjects[areaName].map((project) => (
<div
key={project.id}
className="bg-gray-50 dark:bg-gray-900 rounded-lg shadow-md relative"
style={{ minHeight: "280px", maxHeight: "280px" }}
>
<div
className="bg-gray-200 dark:bg-gray-700 flex items-center justify-center overflow-hidden rounded-t-lg"
style={{ height: "160px" }}
>
<span className="text-2xl font-extrabold text-gray-500 dark:text-gray-400 opacity-20">
{getProjectInitials(project.name)}
</span>
</div>
<div className="flex justify-between items-start p-4">
<Link
to={`/project/${project.id}`}
className="text-lg font-semibold text-gray-900 dark:text-gray-100 hover:underline line-clamp-2"
style={{ minHeight: "3.3rem", maxHeight: "3.3rem" }}
>
{project.name}
</Link>
<div className="relative">
<button
className="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-400 focus:outline-none"
onClick={() =>
setActiveDropdown(
activeDropdown === project.id
? null
: project.id ?? null
)
}
>
<EllipsisVerticalIcon className="h-5 w-5" />
</button>
{activeDropdown === project.id && (
<div className="absolute right-0 mt-2 w-28 bg-white dark:bg-gray-700 shadow-lg rounded-md z-10">
<button
onClick={() => handleEditProject(project)}
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left"
>
Edit
</button>
<button
onClick={() => {
setProjectToDelete(project);
setIsConfirmDialogOpen(true);
setActiveDropdown(null);
}}
className="block px-4 py-2 text-sm text-red-500 dark:text-red-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left"
>
Delete
</button>
</div>
)}
</div>
</div>
<div className="absolute bottom-4 left-0 right-0 px-4">
<div className="flex items-center space-x-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
style={{
width: `${getCompletionPercentage(project?.id)}%`,
}}
></div>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400">
{getCompletionPercentage(project?.id)}%
</span>
</div>
</div>
</div>
))}
{viewMode === "cards" && (
<h3 className="col-span-full text-md uppercase font-light text-gray-800 dark:text-gray-200 mb-2 mt-6">
{areaName}
</h3>
)}
{groupedProjects[areaName].map((project) => {
const { color } = getPriorityStyles(project.priority || "low");
return (
<ProjectItem
key={project.id}
project={project}
viewMode={viewMode}
color={color}
getCompletionPercentage={getCompletionPercentage}
activeDropdown={activeDropdown}
setActiveDropdown={setActiveDropdown}
handleEditProject={handleEditProject}
setProjectToDelete={setProjectToDelete}
setIsConfirmDialogOpen={setIsConfirmDialogOpen}
/>
);
})}
</React.Fragment>
))
)}
</div>
</div>
{/* Project Modal */}
{isProjectModalOpen && (
<ProjectModal
isOpen={isProjectModalOpen}
@ -326,7 +301,6 @@ const Projects: React.FC = () => {
/>
)}
{/* Delete Confirmation Dialog */}
{isConfirmDialogOpen && (
<ConfirmDialog
title="Delete Project"

View file

@ -27,11 +27,11 @@ const useFetchProjects = (options?: UseFetchProjectsOptions): UseFetchProjectsRe
let url = '/api/projects';
const params = new URLSearchParams();
if (options?.activeFilter !== undefined) {
if (options?.activeFilter !== undefined && options.activeFilter !== "all") {
params.append('active', String(options.activeFilter));
}
if (options?.areaFilter !== undefined) {
params.append('area', options.areaFilter);
params.append('area_id', options.areaFilter);
}
if (params.toString()) {

View file

@ -9,7 +9,7 @@ class Task < ActiveRecord::Base
# Existing scopes
scope :complete, -> { where(status: statuses[:done]) }
scope :incomplete, -> { where.not(status: statuses[:done]) }
scope :due_today, -> { incomplete.where('DATE(due_date) < ?', Date.today) }
scope :due_today, -> { incomplete.where('DATE(due_date) <= ?', Date.today) }
scope :upcoming, -> { incomplete.where('due_date BETWEEN ? AND ?', Date.today, Date.today + 7.days) }
scope :someday, -> { incomplete.where(due_date: nil) }
scope :next_actions, -> { incomplete.where(due_date: nil, project_id: nil) }
@ -85,13 +85,27 @@ class Task < ActiveRecord::Base
tasks_due_today = user.tasks.due_today
# Suggested tasks
# Exclude tasks that are in progress or due today
excluded_task_ids = tasks_in_progress.pluck(:id) + tasks_due_today.pluck(:id)
suggested_tasks = user.tasks.incomplete
.where(status: statuses[:not_started])
.where.not(id: excluded_task_ids)
.order_by_priority
.limit(5)
# Fetch suggested tasks not in projects, ordered by task priority
tasks_without_projects = user.tasks.incomplete
.where(status: statuses[:not_started], project_id: nil)
.where.not(id: excluded_task_ids)
.order(priority: :desc)
.limit(3)
# Fetch suggested tasks in projects, ordered by task priority and project priority
tasks_in_projects = user.tasks.incomplete
.where(status: statuses[:not_started])
.where.not(project_id: nil)
.where.not(id: excluded_task_ids)
.joins('LEFT JOIN projects ON tasks.project_id = projects.id')
.order(
Arel.sql('tasks.priority DESC, projects.priority DESC')
)
.distinct
.limit(3)
{
total_open_tasks: total_open_tasks,
@ -99,7 +113,7 @@ class Task < ActiveRecord::Base
tasks_in_progress: tasks_in_progress,
tasks_in_progress_count: tasks_in_progress_count,
tasks_due_today: tasks_due_today,
suggested_tasks: suggested_tasks
suggested_tasks: (tasks_without_projects + tasks_in_projects)
}
end

View file

@ -22,7 +22,7 @@ class Sinatra::Application
get '/projects' do
active_param = params[:active]
is_active = active_param == 'true' unless active_param.nil?
is_active = active_param == 'true' unless active_param.nil? || active_param == 'all'
pin_to_sidebar_param = params[:pin_to_sidebar]
is_pinned = pin_to_sidebar_param == 'true' unless pin_to_sidebar_param.nil?
@ -37,7 +37,7 @@ class Sinatra::Application
projects = projects.where(active: is_active) unless is_active.nil?
projects = projects.where(pin_to_sidebar: is_pinned) unless is_pinned.nil?
projects = projects.where(area_id: area_id_param) if area_id_param
projects = projects.where(area_id: area_id_param) unless area_id_param.blank?
task_status_counts = projects.each_with_object({}) do |project, counts|
counts[project.id] = project.task_status_counts
end

File diff suppressed because one or more lines are too long