Отслеживание просмотров задач: task_views, POST /api/tasks/:id/view и обновление /api/home/recent

This commit is contained in:
2026-08-24 15:40:30 +03:00
parent 74abd8fad5
commit a4eaf65915
7 changed files with 107 additions and 1 deletions

View File

@@ -270,6 +270,13 @@ homeRouter.get('/api/home/recent',
WHERE author_id = $2
AND task_id IN (SELECT id FROM tasks WHERE organization_id = $1)
GROUP BY task_id
UNION ALL
SELECT task_id, MAX(viewed_at) AS last_at
FROM task_views
WHERE organization_id = $1 AND user_id = $2
GROUP BY task_id
),
recent_max AS (
SELECT task_id, MAX(last_at) AS last_at
@@ -322,6 +329,8 @@ homeRouter.get('/api/home/recent',
SELECT task_id FROM task_messages
WHERE author_id = $2
AND task_id IN (SELECT id FROM tasks WHERE organization_id = $1)
UNION
SELECT task_id FROM task_views WHERE organization_id = $1 AND user_id = $2
),
accessible AS (
SELECT task_id FROM task_user_access

View File

@@ -21,6 +21,29 @@ export function registerTaskCrudExtraRoutes(router: ReturnType<typeof import("ex
}
});
router.post('/api/tasks/:id/view', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
const userId = req.user!.id;
const organizationId = req.organizationId!;
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
// Make sure the task exists in this organization before recording a view.
const task = await storage.getTask(taskId, organizationId);
if (!task) {
return res.status(404).json({ success: false, error: 'Задача не найдена' });
}
await storage.recordTaskView(taskId, userId, organizationId);
res.json({ success: true });
} catch (err) {
console.error('[tasks/:id/view]', err);
res.status(500).json({ success: false, error: 'Ошибка сервера' });
}
});
router.get('/api/tasks/:id/audit-log', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);

View File

@@ -423,6 +423,9 @@ export interface IStorage {
addTaskAuditLog(entry: InsertTaskAuditLog): Promise<TaskAuditLog>;
getTaskAuditLog(taskId: number, organizationId: number): Promise<TaskAuditLog[]>;
// Task Views
recordTaskView(taskId: number, userId: number, organizationId: number): Promise<void>;
// Roles
getRoles(organizationId: number): Promise<(import('@shared/schema').Role & { memberCount: number; members: { id: number; userId: number; user: { id: number; firstName: string; lastName: string; email: string } }[]; manager: { id: number; firstName: string; lastName: string; middleName: string | null; email: string } | null })[]>;
getRole(id: number, organizationId: number): Promise<import('@shared/schema').Role | undefined>;

View File

@@ -1,4 +1,4 @@
import { users, forms, tasks, taskMessages, messageReads, bots, roles, type User, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type Task, type TaskFieldValue, type TaskMessage, type TaskMessageWithAuthor, type MessageRead, type DataTableRow } from "@shared/schema";
import { users, forms, tasks, taskMessages, messageReads, bots, roles, taskViews, type User, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type Task, type TaskFieldValue, type TaskMessage, type TaskMessageWithAuthor, type MessageRead, type DataTableRow } from "@shared/schema";
import { taskRelations } from "@shared/schema";
import { taskAssignees, type TaskAssignee } from "@shared/schema";
import { db } from "../db";
@@ -614,4 +614,24 @@ export class TasksStorage extends TasksCoreStorage {
.where(and(eq(taskAssignees.taskId, taskId), eq(taskAssignees.organizationId, organizationId)));
return rows;
}
// =====================
// Task Views (recently viewed tasks for the home "Recent" tab)
// =====================
async recordTaskView(taskId: number, userId: number, organizationId: number): Promise<void> {
await db
.insert(taskViews)
.values({
taskId,
userId,
organizationId,
})
.onConflictDoUpdate({
target: [taskViews.organizationId, taskViews.userId, taskViews.taskId],
set: {
viewedAt: sql`CURRENT_TIMESTAMP`,
},
});
}
}