Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com> Co-authored-by: Vigers Ray <vigersray@gmail.com>
434 lines
16 KiB
C#
434 lines
16 KiB
C#
using System.Linq;
|
||
using Content.Client.Administration.Systems;
|
||
using Content.Client.Administration.UI.Bwoink;
|
||
using Content.Shared._Sunrise.MentorHelp;
|
||
using Content.Shared.Administration;
|
||
using Content.Shared.Database;
|
||
using Robust.Client.AutoGenerated;
|
||
using Robust.Client.State;
|
||
using Robust.Client.UserInterface;
|
||
using Robust.Client.UserInterface.Controls;
|
||
using Robust.Client.UserInterface.XAML;
|
||
using Robust.Shared.Network;
|
||
using Robust.Shared.Timing;
|
||
|
||
namespace Content.Client._Sunrise.MentorHelp
|
||
{
|
||
/// <summary>
|
||
/// Main control for mentor help interface
|
||
/// </summary>
|
||
[GenerateTypedNameReferences]
|
||
public sealed partial class MentorHelpControl : Control
|
||
{
|
||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||
[Dependency] private readonly IGameTiming _timing = default!;
|
||
|
||
private MentorHelpSystem? _mentorHelpSystem;
|
||
private NetUserId _ownerUserId;
|
||
private bool _hasMentorPermissions;
|
||
private List<MentorHelpTicketData> _tickets = new();
|
||
private MentorHelpTicketData? _selectedTicket;
|
||
private Dictionary<int, List<MentorHelpMessageData>> _ticketMessages = new();
|
||
|
||
private readonly Dictionary<int, TicketEntryControl> _openTicketControls = new();
|
||
private readonly Dictionary<int, TicketEntryControl> _closedTicketControls = new();
|
||
|
||
private enum ViewState
|
||
{
|
||
TicketsList,
|
||
TicketView
|
||
}
|
||
|
||
private MentorHelpNewTicketDialog? _newTicketDialog;
|
||
private int? _pendingOpenTicketId;
|
||
|
||
public MentorHelpControl()
|
||
{
|
||
RobustXamlLoader.Load(this);
|
||
IoCManager.InjectDependencies(this);
|
||
|
||
// Wire up button events
|
||
NewTicketButton.OnPressed += _ => OpenNewTicketDialog();
|
||
StatisticsButton.OnPressed += _ => _ui.GetUIController<MentorHelpStatisticsUIController>().ToggleStatistics();
|
||
BackToListButton.OnPressed += _ => SwitchState(ViewState.TicketsList);
|
||
|
||
// Wire up ticket action buttons
|
||
ClaimButton.OnPressed += _ => ClaimTicket();
|
||
UnassignButton.OnPressed += _ => UnassignTicket();
|
||
CloseTicketButton.OnPressed += _ => CloseTicket();
|
||
|
||
// Handle enter key in reply input
|
||
ReplyInput.OnTextEntered += _ => SendReply();
|
||
|
||
// Setup tab container like in AdminMenuWindow
|
||
TicketsTabContainer.SetTabTitle(0, Loc.GetString("mentor-help-tab-open"));
|
||
TicketsTabContainer.SetTabTitle(1, Loc.GetString("mentor-help-tab-closed"));
|
||
|
||
// Handle tab changes to load closed tickets when needed
|
||
TicketsTabContainer.OnTabChanged += OnTabChanged;
|
||
|
||
AdminWhoButton.OnPressed += _ =>
|
||
{
|
||
var ctrl = _ui.GetUIController<AdminWhoUIController>();
|
||
ctrl.Toggle();
|
||
};
|
||
|
||
// Set initial state
|
||
SwitchState(ViewState.TicketsList);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Попытаться открыть тикет с указанным id. Если тикет ещё не загружен, запросит список тикетов
|
||
/// и откроет тикет когда список придёт.
|
||
/// </summary>
|
||
public void TryOpenTicket(int ticketId)
|
||
{
|
||
// If we already have the ticket loaded, select it immediately
|
||
var ticket = _tickets.FirstOrDefault(t => t.Id == ticketId);
|
||
if (ticket != null)
|
||
{
|
||
OnTicketSelected(ticket);
|
||
return;
|
||
}
|
||
|
||
// Otherwise, remember to open when the list arrives and request tickets from server
|
||
_pendingOpenTicketId = ticketId;
|
||
_mentorHelpSystem?.RequestTickets(onlyMine: !_hasMentorPermissions);
|
||
}
|
||
|
||
public void Initialize(MentorHelpSystem? mentorHelpSystem, NetUserId ownerUserId, bool hasMentorPermissions)
|
||
{
|
||
_mentorHelpSystem = mentorHelpSystem;
|
||
_ownerUserId = ownerUserId;
|
||
_hasMentorPermissions = hasMentorPermissions;
|
||
|
||
// Show/hide buttons based on permissions
|
||
StatisticsButton.Visible = _hasMentorPermissions;
|
||
UpdateTicketActionButtons();
|
||
}
|
||
|
||
private void OnTabChanged(int tabIndex)
|
||
{
|
||
// When switching to closed tickets tab, request all tickets including closed ones
|
||
if (tabIndex == 1 && _hasMentorPermissions)
|
||
{
|
||
_mentorHelpSystem?.RequestTickets(onlyMine: false);
|
||
}
|
||
else if (tabIndex == 1 && !_hasMentorPermissions)
|
||
{
|
||
_mentorHelpSystem?.RequestTickets(onlyMine: true);
|
||
}
|
||
}
|
||
|
||
// State switching like in LobbyGui
|
||
private void SwitchState(ViewState state)
|
||
{
|
||
DefaultState.Visible = false;
|
||
TicketViewState.Visible = false;
|
||
BackToListButton.Visible = false;
|
||
|
||
switch (state)
|
||
{
|
||
case ViewState.TicketsList:
|
||
DefaultState.Visible = true;
|
||
_selectedTicket = null;
|
||
break;
|
||
case ViewState.TicketView:
|
||
TicketViewState.Visible = true;
|
||
BackToListButton.Visible = true;
|
||
UpdateTicketHeader();
|
||
UpdateTicketActionButtons();
|
||
break;
|
||
}
|
||
}
|
||
|
||
public void UpdateTicketsList(List<MentorHelpTicketData> tickets)
|
||
{
|
||
_tickets = tickets;
|
||
RefreshTicketsList();
|
||
|
||
// If we were asked to open a specific ticket once the list arrives, do it now
|
||
if (_pendingOpenTicketId.HasValue)
|
||
{
|
||
var id = _pendingOpenTicketId.Value;
|
||
_pendingOpenTicketId = null;
|
||
var ticket = _tickets.FirstOrDefault(t => t.Id == id);
|
||
if (ticket != null)
|
||
{
|
||
OnTicketSelected(ticket);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Refresh tickets like in ContributorsTop
|
||
private void RefreshTicketsList()
|
||
{
|
||
var openTickets = _tickets.Where(t => t.Status != MentorHelpTicketStatus.Closed).ToList();
|
||
var closedTickets = _tickets.Where(t => t.Status == MentorHelpTicketStatus.Closed).ToList();
|
||
|
||
// Filter for players to show only their tickets
|
||
if (!_hasMentorPermissions)
|
||
{
|
||
openTickets = openTickets.Where(t => t.PlayerId == _ownerUserId).ToList();
|
||
closedTickets = closedTickets.Where(t => t.PlayerId == _ownerUserId).ToList();
|
||
}
|
||
|
||
// Update open tickets
|
||
RefreshTicketTab(openTickets, _openTicketControls, OpenTicketsList);
|
||
|
||
// Update closed tickets
|
||
RefreshTicketTab(closedTickets, _closedTicketControls, ClosedTicketsList);
|
||
}
|
||
|
||
private void RefreshTicketTab(List<MentorHelpTicketData> tickets, Dictionary<int, TicketEntryControl> controls, BoxContainer container)
|
||
{
|
||
var sortedTickets = tickets.OrderByDescending(t => t.UpdatedAt).ToList();
|
||
|
||
// Remove tickets that no longer exist
|
||
var toRemove = controls.Keys.Where(id => !sortedTickets.Any(t => t.Id == id)).ToList();
|
||
foreach (var id in toRemove)
|
||
{
|
||
if (controls.TryGetValue(id, out var control))
|
||
{
|
||
container.RemoveChild(control);
|
||
controls.Remove(id);
|
||
}
|
||
}
|
||
|
||
// Add or update existing tickets
|
||
foreach (var ticket in sortedTickets)
|
||
{
|
||
if (controls.TryGetValue(ticket.Id, out var existingControl))
|
||
{
|
||
existingControl.UpdateData(ticket);
|
||
}
|
||
else
|
||
{
|
||
var control = new TicketEntryControl();
|
||
control.UpdateData(ticket);
|
||
control.OnTicketSelected += OnTicketSelected;
|
||
controls[ticket.Id] = control;
|
||
container.AddChild(control);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnTicketSelected(MentorHelpTicketData ticket)
|
||
{
|
||
_selectedTicket = ticket;
|
||
SwitchState(ViewState.TicketView);
|
||
|
||
// Clear previous messages first
|
||
MessagesContainer.RemoveAllChildren();
|
||
|
||
// Request messages for this ticket
|
||
_mentorHelpSystem?.RequestTicketMessages(ticket.Id);
|
||
}
|
||
|
||
public void UpdateTicketMessages(int ticketId, List<MentorHelpMessageData> messages)
|
||
{
|
||
_ticketMessages[ticketId] = messages;
|
||
|
||
if (_selectedTicket?.Id == ticketId && TicketViewState.Visible)
|
||
{
|
||
DisplayTicketMessages(messages);
|
||
}
|
||
}
|
||
|
||
public void UpdateTicket(MentorHelpTicketData ticket)
|
||
{
|
||
// Update ticket in our list
|
||
var index = _tickets.FindIndex(t => t.Id == ticket.Id);
|
||
if (index >= 0)
|
||
{
|
||
_tickets[index] = ticket;
|
||
}
|
||
else
|
||
{
|
||
_tickets.Add(ticket);
|
||
}
|
||
|
||
// Update selected ticket if it's the one that changed
|
||
if (_selectedTicket?.Id == ticket.Id)
|
||
{
|
||
_selectedTicket = ticket;
|
||
if (TicketViewState.Visible)
|
||
{
|
||
UpdateTicketHeader();
|
||
UpdateTicketActionButtons();
|
||
}
|
||
}
|
||
|
||
// If admin closed the ticket while player was viewing it, return to list
|
||
if (ticket.Status == MentorHelpTicketStatus.Closed &&
|
||
_selectedTicket?.Id == ticket.Id &&
|
||
!_hasMentorPermissions &&
|
||
TicketViewState.Visible)
|
||
{
|
||
SwitchState(ViewState.TicketsList);
|
||
}
|
||
|
||
// Refresh the list to show updated status
|
||
RefreshTicketsList();
|
||
}
|
||
|
||
private void UpdateTicketHeader()
|
||
{
|
||
if (_selectedTicket == null)
|
||
return;
|
||
|
||
// Отдельные метки для ID и темы (чтобы занимали меньше места и были выровнены)
|
||
TicketIdLabel.Text = $"#{_selectedTicket.Id}";
|
||
TicketSubjectLabel.Text = _selectedTicket.Subject;
|
||
|
||
// Компактная строка: статус | назначен | создано
|
||
TicketStatus.Text = Loc.GetString("mentor-help-status-label", ("status", GetStatusText(_selectedTicket.Status)));
|
||
TicketAssigned.Text = Loc.GetString("mentor-help-assigned-label",
|
||
("assigned", _selectedTicket.AssignedToName ?? Loc.GetString("mentor-help-unassigned")));
|
||
TicketCreated.Text = Loc.GetString("mentor-help-created-label",
|
||
("created", _selectedTicket.CreatedAt.ToString("dd.MM.yyyy HH:mm")));
|
||
}
|
||
|
||
private void UpdateTicketActionButtons()
|
||
{
|
||
if (_selectedTicket == null)
|
||
{
|
||
ReplyPanel.Visible = false;
|
||
return;
|
||
}
|
||
|
||
var canReply = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||
var isAssignedToMe = _selectedTicket.AssignedToUserId == _ownerUserId;
|
||
var isOpen = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||
|
||
// Hide reply panel for closed tickets
|
||
ReplyPanel.Visible = canReply;
|
||
|
||
if (_hasMentorPermissions)
|
||
{
|
||
ClaimButton.Visible = isOpen && !isAssignedToMe;
|
||
UnassignButton.Visible = isOpen && isAssignedToMe;
|
||
CloseTicketButton.Visible = isOpen;
|
||
}
|
||
else
|
||
{
|
||
ClaimButton.Visible = false;
|
||
UnassignButton.Visible = false;
|
||
CloseTicketButton.Visible = isOpen; // Players can close their own tickets
|
||
}
|
||
}
|
||
|
||
private string GetStatusText(MentorHelpTicketStatus status)
|
||
{
|
||
return status switch
|
||
{
|
||
MentorHelpTicketStatus.Open => Loc.GetString("mentor-help-status-open"),
|
||
MentorHelpTicketStatus.Assigned => Loc.GetString("mentor-help-status-assigned"),
|
||
MentorHelpTicketStatus.AwaitingResponse => Loc.GetString("mentor-help-status-awaiting"),
|
||
MentorHelpTicketStatus.Closed => Loc.GetString("mentor-help-status-closed"),
|
||
_ => Loc.GetString("mentor-help-status-unknown")
|
||
};
|
||
}
|
||
|
||
private void DisplayTicketMessages(List<MentorHelpMessageData> messages)
|
||
{
|
||
MessagesContainer.RemoveAllChildren();
|
||
|
||
DateTime? lastDate = null;
|
||
|
||
foreach (var message in messages.OrderBy(m => m.SentAt))
|
||
{
|
||
// Ensure we show a date header when the day changes
|
||
var sentAt = message.SentAt;
|
||
var sentDate = sentAt.Date;
|
||
if (lastDate == null || lastDate.Value != sentDate)
|
||
{
|
||
lastDate = sentDate;
|
||
var dateLabel = new RichTextLabel
|
||
{
|
||
Text = $"[center=\"{sentAt:dd.MM.yyyy}\"]",
|
||
HorizontalExpand = true
|
||
};
|
||
|
||
MessagesContainer.AddChild(dateLabel);
|
||
}
|
||
|
||
var messageBox = new PanelContainer
|
||
{
|
||
StyleClasses = { "PanelColorMedium" },
|
||
HorizontalExpand = true,
|
||
Margin = new Thickness(0, 2)
|
||
};
|
||
|
||
var vbox = new BoxContainer
|
||
{
|
||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||
HorizontalExpand = true
|
||
};
|
||
|
||
// Format: [HH:mm] Author: message
|
||
var content = new RichTextLabel
|
||
{
|
||
Text = $"[bold]{sentAt:HH:mm}[/bold] {message.FormattedSender}: {message.Message}",
|
||
HorizontalExpand = true
|
||
};
|
||
|
||
vbox.AddChild(content);
|
||
messageBox.AddChild(vbox);
|
||
MessagesContainer.AddChild(messageBox);
|
||
}
|
||
}
|
||
|
||
private void OpenNewTicketDialog()
|
||
{
|
||
if (_newTicketDialog != null)
|
||
{
|
||
_newTicketDialog.Close();
|
||
_newTicketDialog = null;
|
||
return;
|
||
}
|
||
|
||
_newTicketDialog = new MentorHelpNewTicketDialog();
|
||
_newTicketDialog.OnTicketCreated += (subject, message) =>
|
||
{
|
||
_newTicketDialog?.Close();
|
||
_mentorHelpSystem?.CreateTicket(subject, message);
|
||
};
|
||
_newTicketDialog.OnClose += () => _newTicketDialog = null;
|
||
_newTicketDialog.OpenCentered();
|
||
}
|
||
|
||
private void SendReply()
|
||
{
|
||
if (_selectedTicket == null || string.IsNullOrWhiteSpace(ReplyInput.Text))
|
||
return;
|
||
|
||
_mentorHelpSystem?.ReplyToTicket(_selectedTicket.Id, ReplyInput.Text.Trim());
|
||
ReplyInput.Text = string.Empty;
|
||
}
|
||
|
||
private void ClaimTicket()
|
||
{
|
||
if (_selectedTicket == null)
|
||
return;
|
||
|
||
_mentorHelpSystem?.ClaimTicket(_selectedTicket.Id);
|
||
}
|
||
|
||
private void UnassignTicket()
|
||
{
|
||
if (_selectedTicket == null)
|
||
return;
|
||
|
||
_mentorHelpSystem?.UnassignTicket(_selectedTicket.Id);
|
||
}
|
||
|
||
private void CloseTicket()
|
||
{
|
||
if (_selectedTicket == null)
|
||
return;
|
||
|
||
_mentorHelpSystem?.CloseTicket(_selectedTicket.Id);
|
||
}
|
||
}
|
||
}
|