feat(discord): add DiscordRescheduleVotingRenderer and replace inline helper
This commit is contained in:
@@ -2,6 +2,7 @@ namespace GmRelay.DiscordBot.Features.Sessions;
|
|||||||
|
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using GmRelay.DiscordBot.Infrastructure.Discord;
|
using GmRelay.DiscordBot.Infrastructure.Discord;
|
||||||
|
using GmRelay.DiscordBot.Rendering;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
@@ -89,7 +90,7 @@ public sealed class DiscordRescheduleHandler(
|
|||||||
var optionDtos = options.Select((o, i) => new RescheduleOptionDto(Guid.NewGuid(), i + 1, o)).ToList();
|
var optionDtos = options.Select((o, i) => new RescheduleOptionDto(Guid.NewGuid(), i + 1, o)).ToList();
|
||||||
|
|
||||||
// 7. Build and send Discord vote message BEFORE transaction
|
// 7. Build and send Discord vote message BEFORE transaction
|
||||||
var (embed, actionRow) = BuildVoteMessage(session.Title, session.CurrentScheduledAt, deadline, optionDtos, participants, []);
|
var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(session.Title, session.CurrentScheduledAt, deadline, optionDtos, participants, []);
|
||||||
|
|
||||||
var channelIdUlong = ulong.Parse(channelId);
|
var channelIdUlong = ulong.Parse(channelId);
|
||||||
|
|
||||||
@@ -149,45 +150,6 @@ public sealed class DiscordRescheduleHandler(
|
|||||||
return new DiscordRescheduleResult(proposalId, optionDtos, deadline);
|
return new DiscordRescheduleResult(proposalId, optionDtos, deadline);
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal for now — temporary duplication until DiscordRescheduleVotingRenderer is extracted in Task 6
|
|
||||||
internal static (EmbedProperties Embed, ActionRowProperties ActionRow) BuildVoteMessage(
|
|
||||||
string title, DateTime currentTime, DateTimeOffset deadline,
|
|
||||||
IReadOnlyList<RescheduleOptionDto> options, IReadOnlyList<VoteParticipantDto> participants, IReadOnlyList<RescheduleOptionVoteDto> votes)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
sb.AppendLine($"📅 Текущее время: {currentTime.FormatMoscow()} (МСК)");
|
|
||||||
sb.AppendLine($"⏳ Дедлайн: {deadline.FormatMoscow()} (МСК)");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("Выберите один из вариантов:");
|
|
||||||
|
|
||||||
foreach (var option in options.OrderBy(o => o.DisplayOrder))
|
|
||||||
{
|
|
||||||
var count = votes.Count(v => v.OptionId == option.OptionId);
|
|
||||||
sb.AppendLine($"{option.DisplayOrder}. **{option.ProposedAt.FormatMoscow()}** (МСК) — {count} голосов");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (participants.Count > 0)
|
|
||||||
{
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine($"Голосов: {votes.Count}/{participants.Count}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var embed = new EmbedProperties()
|
|
||||||
.WithTitle($"🔄 Перенос сессии «{title}»")
|
|
||||||
.WithDescription(sb.ToString())
|
|
||||||
.WithColor(new NetCord.Color(0xFEE75C));
|
|
||||||
|
|
||||||
var actionRow = new ActionRowProperties();
|
|
||||||
foreach (var option in options.OrderBy(o => o.DisplayOrder))
|
|
||||||
{
|
|
||||||
actionRow.Add(new ButtonProperties(
|
|
||||||
$"reschedule_vote:{option.OptionId}",
|
|
||||||
$"{option.DisplayOrder}. {option.ProposedAt.ToOffset(TimeSpan.FromHours(3)):dd.MM HH:mm}",
|
|
||||||
ButtonStyle.Primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (embed, actionRow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record RescheduleSessionInfoDto(string Title, DateTime CurrentScheduledAt);
|
internal sealed record RescheduleSessionInfoDto(string Title, DateTime CurrentScheduledAt);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
namespace GmRelay.DiscordBot.Features.Sessions;
|
namespace GmRelay.DiscordBot.Features.Sessions;
|
||||||
|
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GmRelay.DiscordBot.Rendering;
|
||||||
using GmRelay.Shared.Domain;
|
using GmRelay.Shared.Domain;
|
||||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
@@ -105,7 +106,7 @@ public sealed class DiscordRescheduleVoteHandler(
|
|||||||
await transaction.CommitAsync(ct);
|
await transaction.CommitAsync(ct);
|
||||||
|
|
||||||
// 5. Re-render and update Discord vote message
|
// 5. Re-render and update Discord vote message
|
||||||
var (embed, actionRow) = DiscordRescheduleHandler.BuildVoteMessage(
|
var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(
|
||||||
proposal.Title, proposal.CurrentScheduledAt, proposal.VotingDeadlineAt,
|
proposal.Title, proposal.CurrentScheduledAt, proposal.VotingDeadlineAt,
|
||||||
options, participants, votes);
|
options, participants, votes);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
namespace GmRelay.DiscordBot.Rendering;
|
||||||
|
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||||
|
using NetCord;
|
||||||
|
using NetCord.Rest;
|
||||||
|
|
||||||
|
public static class DiscordRescheduleVotingRenderer
|
||||||
|
{
|
||||||
|
public static (EmbedProperties Embed, ActionRowProperties ActionRow) Render(
|
||||||
|
string title,
|
||||||
|
DateTime currentTime,
|
||||||
|
DateTimeOffset deadline,
|
||||||
|
IReadOnlyList<RescheduleOptionDto> options,
|
||||||
|
IReadOnlyList<VoteParticipantDto> participants,
|
||||||
|
IReadOnlyList<RescheduleOptionVoteDto> votes)
|
||||||
|
{
|
||||||
|
var votesByOption = votes.GroupBy(v => v.OptionId).ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
var votedPlayerIds = votes.Select(v => v.PlayerId).ToHashSet();
|
||||||
|
var pending = participants.Where(p => !votedPlayerIds.Contains(p.PlayerId)).Select(p => p.DisplayName).ToList();
|
||||||
|
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine($"📅 Текущее время: {currentTime.FormatMoscow()} (МСК)");
|
||||||
|
sb.AppendLine($"⏳ Дедлайн: {deadline.FormatMoscow()} (МСК)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Выберите один из вариантов:");
|
||||||
|
|
||||||
|
foreach (var option in options.OrderBy(o => o.DisplayOrder))
|
||||||
|
{
|
||||||
|
var optionVotes = votesByOption.GetValueOrDefault(option.OptionId, []);
|
||||||
|
sb.AppendLine($"{option.DisplayOrder}. **{option.ProposedAt.FormatMoscow()}** (МСК) — {optionVotes.Count} голосов");
|
||||||
|
if (optionVotes.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" {string.Join(", ", optionVotes.Select(v => v.DisplayName))}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Не проголосовали: {string.Join(", ", pending)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Голосов: {votedPlayerIds.Count}/{participants.Count}");
|
||||||
|
sb.AppendLine("Правило: побеждает вариант с большинством голосов к дедлайну; при ничьей перенос не применяется.");
|
||||||
|
|
||||||
|
var embed = new EmbedProperties()
|
||||||
|
.WithTitle($"🔄 Перенос сессии «{title}»")
|
||||||
|
.WithDescription(sb.ToString())
|
||||||
|
.WithColor(new Color(0xFEE75C));
|
||||||
|
|
||||||
|
var actionRow = new ActionRowProperties();
|
||||||
|
foreach (var option in options.OrderBy(o => o.DisplayOrder))
|
||||||
|
{
|
||||||
|
actionRow.Add(new ButtonProperties(
|
||||||
|
$"reschedule_vote:{option.OptionId}",
|
||||||
|
$"{option.DisplayOrder}. {FormatButtonTime(option.ProposedAt)}",
|
||||||
|
ButtonStyle.Primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (embed, actionRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatButtonTime(DateTimeOffset utc)
|
||||||
|
=> utc.ToOffset(TimeSpan.FromHours(3)).ToString("dd.MM HH:mm", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user