diff --git a/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs b/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs index 8a7dbe4..338ea7e 100644 --- a/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs +++ b/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs @@ -188,9 +188,11 @@ public sealed class PnvBotUpdateHandler( : await sender.Send(new RejectTelegramLoginCommand(requestId, fromId), cancellationToken); await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); - if (result.IsSuccess && callback.Message is not null) + if (callback.Message is not null) { - var statusText = parts[1] == "approve" ? "✅ Вход подтверждён." : "❌ Вход отклонён."; + var statusText = result.IsSuccess + ? (parts[1] == "approve" ? "✅ Вход подтверждён." : "❌ Вход отклонён.") + : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); @@ -211,11 +213,14 @@ public sealed class PnvBotUpdateHandler( : await sender.Send(new RejectActivationCommand(requestId, Reason: null), cancellationToken); await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); - if (result.IsSuccess && callback.Message is not null) + if (callback.Message is not null) { // Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки - // «Активировать/Отклонить» остаются висеть под уже обработанным запросом. - var statusText = parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён."; + // «Активировать/Отклонить» остаются висеть под уже обработанным запросом (в т.ч. + // если его обработали в другом месте — на сайте или из другого чата). + var statusText = result.IsSuccess + ? (parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён.") + : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); @@ -236,9 +241,11 @@ public sealed class PnvBotUpdateHandler( : await sender.Send(new RejectRoleRequestCommand(requestId, Reason: null), cancellationToken); await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); - if (result.IsSuccess && callback.Message is not null) + if (callback.Message is not null) { - var statusText = parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена."; + var statusText = result.IsSuccess + ? (parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена.") + : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs index 4d51d97..c8be340 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs @@ -106,9 +106,13 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe if (!link.IsLinked || link.TelegramUserId is not { } telegramUserId) return; + InlineKeyboardMarkup? keyboard = null; + if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) + keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", options.Value.PublicSiteUrl) }); + try { - await botClient.SendMessage(telegramUserId, message, cancellationToken: cancellationToken); + await botClient.SendMessage(telegramUserId, message, replyMarkup: keyboard, cancellationToken: cancellationToken); } catch { diff --git a/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs index 709fee5..1cede90 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs @@ -9,7 +9,8 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; -public sealed class CloseTicketCommandHandler(IAppDbContext dbContext, IRealtimeNotifier notifier, ICurrentUser currentUser) +public sealed class CloseTicketCommandHandler( + IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) : ICommandHandler { public async Task Handle(CloseTicketCommand command, CancellationToken cancellationToken) @@ -30,6 +31,7 @@ public sealed class CloseTicketCommandHandler(IAppDbContext dbContext, IRealtime adminId, "TicketClosed", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); + await telegramNotifier.NotifyUserAsync(ticket.UserId, "🔒 Ваше обращение закрыто.", cancellationToken); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs index e50d65a..e06f765 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs @@ -9,7 +9,8 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; -public sealed class ResolveTicketCommandHandler(IAppDbContext dbContext, IRealtimeNotifier notifier, ICurrentUser currentUser) +public sealed class ResolveTicketCommandHandler( + IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) : ICommandHandler { public async Task Handle(ResolveTicketCommand command, CancellationToken cancellationToken) @@ -30,6 +31,7 @@ public sealed class ResolveTicketCommandHandler(IAppDbContext dbContext, IRealti adminId, "TicketResolved", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); + await telegramNotifier.NotifyUserAsync(ticket.UserId, "✅ Ваше обращение решено.", cancellationToken); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs index 091c92d..372d49e 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs @@ -10,7 +10,8 @@ public interface ITelegramNotifier Task NotifyAdminsActivationRequestedAsync( Guid requestId, string userName, string? comment, CancellationToken cancellationToken); - /// Личное сообщение пользователю, если у него привязан Telegram (иначе no-op). + /// Личное сообщение пользователю, если у него привязан Telegram (иначе no-op). + /// Если задан PublicSiteUrl — добавляет кнопку-ссылку на сайт. Task NotifyUserAsync(Guid userId, string message, CancellationToken cancellationToken); /// Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там), diff --git a/frontend/src/routes/admin.tsx b/frontend/src/routes/admin.tsx index 0bec13b..6056fcc 100644 --- a/frontend/src/routes/admin.tsx +++ b/frontend/src/routes/admin.tsx @@ -27,13 +27,13 @@ function AdminLayout() { return (

{t('nav.admin')}

-