Fix: Show Projects with due dates in Upcoming view (#928)

* Add projects with due dates to upcoming view

Fetch and include projects with due_date_at in the upcoming range
when type=upcoming is requested. Projects are filtered by ownership
or permissions and exclude completed/archived projects.

Part of #925

* Display upcoming projects in Upcoming view

Add UI to show projects with due dates in the Upcoming view.
Projects are displayed in a separate section below tasks with
links to project details and formatted due dates.

Fixes #925

* Fix prettier formatting errors
This commit is contained in:
Chris 2026-03-09 23:36:14 +02:00 committed by GitHub
parent 358f577576
commit 8fea7020bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 16 deletions

View file

@ -9,6 +9,7 @@ const {
Task,
TaskEvent,
RecurringCompletion,
Project,
sequelize,
} = require('../../models');
const taskRepository = require('./repository');
@ -32,7 +33,9 @@ const { filterTasksByParams } = require('./queries/query-builders');
const {
getSafeTimezone,
getTodayBoundsInUTC,
getUpcomingRangeInUTC,
} = require('../../utils/timezone-utils');
const permissionsService = require('../../services/permissionsService');
const { isValidUid } = require('../../utils/slug-utils');
const {
@ -212,6 +215,34 @@ router.get('/tasks', async (req, res) => {
let tasks = await filterTasksByParams(req.query, userId, timezone);
// Fetch projects with upcoming due dates for the upcoming view
let upcomingProjects = [];
if (type === 'upcoming') {
const safeTimezone = getSafeTimezone(timezone);
const upcomingRange = getUpcomingRangeInUTC(safeTimezone, 7);
const { Op } = require('sequelize');
// Get projects owned by user or shared with user
const ownedOrShared =
await permissionsService.ownershipOrPermissionWhere(
'project',
userId
);
upcomingProjects = await Project.findAll({
where: {
...ownedOrShared,
due_date_at: {
[Op.between]: [upcomingRange.start, upcomingRange.end],
},
status: {
[Op.notIn]: ['completed', 'archived'],
},
},
order: [['due_date_at', 'ASC']],
});
}
if (type === 'upcoming' && groupBy === 'day') {
console.log('[DEBUG] Expanding recurring tasks for /upcoming');
console.log('[DEBUG] Total tasks before expansion:', tasks.length);
@ -299,6 +330,20 @@ router.get('/tasks', async (req, res) => {
response.groupedTasks = serializedGrouped;
}
// Add upcoming projects to response
if (type === 'upcoming' && upcomingProjects.length > 0) {
response.projects = upcomingProjects.map((project) => ({
id: project.id,
uid: project.uid,
name: project.name,
status: project.status,
priority: project.priority,
due_date_at: project.due_date_at,
created_at: project.created_at,
updated_at: project.updated_at,
}));
}
await addDashboardLists(
response,
userId,

View file

@ -44,6 +44,7 @@ const Tasks: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([]);
const projects = useStore((state: any) => state.projectsStore.projects);
const [groupedTasks, setGroupedTasks] = useState<GroupedTasks | null>(null);
const [upcomingProjects, setUpcomingProjects] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
@ -226,6 +227,7 @@ const Tasks: React.FC = () => {
if (resetPagination) {
setTasks(tasksData.tasks || []);
setGroupedTasks(tasksData.groupedTasks || null);
setUpcomingProjects(tasksData.projects || []);
if (!options?.disablePagination) {
const limitToUse = options?.limitOverride ?? limit;
setOffset(limitToUse);
@ -241,6 +243,9 @@ const Tasks: React.FC = () => {
};
});
}
if (tasksData.projects) {
setUpcomingProjects((prev) => [...prev, ...(tasksData.projects || [])]);
}
if (!options?.disablePagination) {
const limitToUse = options?.limitOverride ?? limit;
setOffset((prev) => prev + limitToUse);
@ -896,22 +901,54 @@ const Tasks: React.FC = () => {
Object.keys(groupedTasks).length > 0) ? (
<>
{query.get('type') === 'upcoming' ? (
<GroupedTaskList
tasks={displayTasks}
groupedTasks={groupedTasks}
groupBy="none"
onTaskCreate={handleTaskCreate}
onTaskUpdate={handleTaskUpdate}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
onTaskDelete={handleTaskDelete}
projects={projects}
hideProjectName={false}
onToggleToday={undefined}
showCompletedTasks={showCompleted}
searchQuery={taskSearchQuery}
/>
<>
<GroupedTaskList
tasks={displayTasks}
groupedTasks={groupedTasks}
groupBy="none"
onTaskCreate={handleTaskCreate}
onTaskUpdate={handleTaskUpdate}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
onTaskDelete={handleTaskDelete}
projects={projects}
hideProjectName={false}
onToggleToday={undefined}
showCompletedTasks={showCompleted}
searchQuery={taskSearchQuery}
/>
{upcomingProjects.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">
{t('projects.upcomingProjects', 'Upcoming Projects')}
</h3>
<div className="space-y-2">
{upcomingProjects.map((project) => (
<div
key={project.uid}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-center justify-between">
<a
href={`/project/${project.uid}-${project.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`}
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
{project.name}
</a>
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<span>{t('common.due', 'Due')}: </span>
<span className="font-medium">
{new Date(project.due_date_at).toLocaleDateString()}
</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
</>
) : groupBy === 'project' ? (
<GroupedTaskList
tasks={displayTasks}