From 53ec41ac1192113dcbab7605320f50464338784e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Sun, 17 May 2026 20:07:56 +0500 Subject: [PATCH] fix --- Api/wwwroot/js/kanban.js | 39 ++++++--- Infrastructure/Workers/NotificationWorker.cs | 83 +++++++++++++++----- 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/Api/wwwroot/js/kanban.js b/Api/wwwroot/js/kanban.js index 5fb8b1e..29dde37 100644 --- a/Api/wwwroot/js/kanban.js +++ b/Api/wwwroot/js/kanban.js @@ -272,7 +272,7 @@ function renderEmpty(message) { function renderKanban(board) { kanbanTitle.textContent = board.title || "Kanban"; kanbanBoard.innerHTML = ""; - const canRenameColumns = isActiveProjectTeamAdmin(); + const canManageColumns = isActiveProjectTeamAdmin(); if (!board.id) { renderEmpty("У проекта пока нет канбанов"); @@ -337,23 +337,28 @@ function renderKanban(board) { const editColumnBtn = columnEl.querySelector(".column-edit-btn"); const columnTitle = columnEl.querySelector("[data-column-title]"); - if (!canRenameColumns) { + if (!canManageColumns) { + deleteColumnBtn?.remove(); editColumnBtn?.remove(); - } - - deleteColumnBtn.addEventListener("click", (event) => { - event.stopPropagation(); - deleteColumn(column); - }); + } else { + deleteColumnBtn?.addEventListener("click", (event) => { + event.stopPropagation(); + deleteColumn(column); + }); - editColumnBtn?.addEventListener("click", (event) => { - event.stopPropagation(); - startEditColumnTitle(columnTitle, column); - }); + editColumnBtn?.addEventListener("click", (event) => { + event.stopPropagation(); + startEditColumnTitle(columnTitle, column); + }); + } kanbanBoard.appendChild(columnEl); }); + if (!canManageColumns) { + return; + } + const addColumn = document.createElement("button"); addColumn.className = "add-column-btn"; addColumn.type = "button"; @@ -369,6 +374,11 @@ function renderKanban(board) { ========================= */ async function addColumnToBoard() { + if (!isActiveProjectTeamAdmin()) { + showToast("Создавать колонки может только админ команды"); + return; + } + if (!state.board?.id) { showToast("Сначала выбери канбан"); return; @@ -478,6 +488,11 @@ function startEditColumnTitle(titleEl, column) { } async function deleteColumn(column) { + if (!isActiveProjectTeamAdmin()) { + showToast("Удалять колонки может только админ команды"); + return; + } + const confirmed = confirm( `Удалить колонку «${column.title}» вместе с задачами: ${column.tasks.length}?` ); diff --git a/Infrastructure/Workers/NotificationWorker.cs b/Infrastructure/Workers/NotificationWorker.cs index c2777ac..2cfbfb7 100644 --- a/Infrastructure/Workers/NotificationWorker.cs +++ b/Infrastructure/Workers/NotificationWorker.cs @@ -69,31 +69,72 @@ private async Task HandleMessageAsync(string eventType, string payload, Cancella var (name, message) = BuildNotificationMessage(eventType); var notifications = scope.ServiceProvider.GetRequiredService(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); - var notificationSender = scope.ServiceProvider.GetRequiredService(); - var notification = await notifications.AddAsync( - userId, - taskId, + var recipientUserIds = await ResolveRecipientUserIdsAsync( + scope.ServiceProvider, kanbanId, - name, - message, - cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - await notificationSender.SendToUserAsync( userId, - new - { - id = notification.Id, - userId = userId, - taskId = taskId, - kanbanId = kanbanId, - type = eventType, - name = name, - message = message, - isRead = false, - createdAt = DateTime.UtcNow - }, cancellationToken); + + var createdNotifications = new List<(Guid UserId, Guid NotificationId, DateTime CreatedAt)>(); + + foreach (var recipientUserId in recipientUserIds) + { + var notification = await notifications.AddAsync( + recipientUserId, + taskId, + kanbanId, + name, + message, + cancellationToken); + + createdNotifications.Add((recipientUserId, notification.Id, notification.CreatedAt)); + } + + await unitOfWork.SaveChangesAsync(cancellationToken); + + foreach (var createdNotification in createdNotifications) + { + await notificationSender.SendToUserAsync( + createdNotification.UserId, + new + { + id = createdNotification.NotificationId, + userId = createdNotification.UserId, + taskId = taskId, + kanbanId = kanbanId, + type = eventType, + name = name, + message = message, + isRead = false, + createdAt = createdNotification.CreatedAt + }, + cancellationToken); + } + } + + private static async Task> ResolveRecipientUserIdsAsync( + IServiceProvider serviceProvider, + Guid kanbanId, + Guid fallbackUserId, + CancellationToken cancellationToken) + { + var kanbans = serviceProvider.GetRequiredService(); + var members = serviceProvider.GetRequiredService(); + + var kanban = await kanbans.GetByIdWithProjectAsync(kanbanId, cancellationToken); + if (kanban?.Project is null) + return [fallbackUserId]; + + var teamMembers = await members.GetMembersByTeamIdAsync(kanban.Project.TeamId, cancellationToken); + var recipientUserIds = teamMembers + .Select(member => member.UserId) + .Distinct() + .ToArray(); + + return recipientUserIds.Length > 0 + ? recipientUserIds + : [fallbackUserId]; } private static (string name, string message) BuildNotificationMessage(string eventType)