fix: C#5 compat - replace all ?. with helper methods, remove GetPluginDataDirectory
This commit is contained in:
@@ -13,14 +13,8 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
{
|
{
|
||||||
private ServiceTrackerDataStore GetDataStore()
|
private ServiceTrackerDataStore GetDataStore()
|
||||||
{
|
{
|
||||||
var dataPath = PluginConfigurationHandler.GetPluginDataDirectory(
|
var dataPath = System.IO.Path.Combine(
|
||||||
HostController.HttpContext.Application["Disco.Plugins.ServiceTracker"] as Plugin
|
AppDomain.CurrentDomain.BaseDirectory, "App_Data", "Plugins", "Disco.Plugins.ServiceTracker");
|
||||||
?? new ServiceTrackerPlugin());
|
|
||||||
if (string.IsNullOrEmpty(dataPath))
|
|
||||||
{
|
|
||||||
dataPath = System.IO.Path.Combine(
|
|
||||||
AppDomain.CurrentDomain.BaseDirectory, "App_Data", "Plugins", "Disco.Plugins.ServiceTracker");
|
|
||||||
}
|
|
||||||
return new ServiceTrackerDataStore(dataPath);
|
return new ServiceTrackerDataStore(dataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +71,7 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
DateTime etaParsed;
|
DateTime etaParsed;
|
||||||
if (DateTime.TryParse(HostController.Request.Form["eta"], out etaParsed))
|
if (DateTime.TryParse(HostController.Request.Form["eta"], out etaParsed))
|
||||||
eta = etaParsed;
|
eta = etaParsed;
|
||||||
var currentUser = HostController.HttpContext.User?.Identity?.Name ?? "system";
|
var currentUser = GetCurrentUser();
|
||||||
service.UpdateTicket(jobId, priorityId, locationId, techId, eta, status, summary, currentUser);
|
service.UpdateTicket(jobId, priorityId, locationId, techId, eta, status, summary, currentUser);
|
||||||
return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Dashboard");
|
return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Dashboard");
|
||||||
}
|
}
|
||||||
@@ -93,7 +87,7 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
return new HttpStatusCodeResult(400);
|
return new HttpStatusCodeResult(400);
|
||||||
var content = HostController.Request.Form["note"];
|
var content = HostController.Request.Form["note"];
|
||||||
var noteType = HostController.Request.Form["noteType"] ?? "general";
|
var noteType = HostController.Request.Form["noteType"] ?? "general";
|
||||||
var currentUser = HostController.HttpContext.User?.Identity?.Name ?? "system";
|
var currentUser = GetCurrentUser();
|
||||||
service.AddNote(jobId, currentUser, currentUser, content, noteType);
|
service.AddNote(jobId, currentUser, currentUser, content, noteType);
|
||||||
return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId);
|
return new RedirectResult("/Plugin/Disco.Plugins.ServiceTracker/Detail?id=" + jobId);
|
||||||
}
|
}
|
||||||
@@ -127,19 +121,28 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
sb.AppendLine("JobId,Device,User,Priority,Location,Status,AssignedTech,OpenedDate,ETA,SlaDeadline,SlaBreached,AgeDays,Summary,NoteCount");
|
sb.AppendLine("JobId,Device,User,Priority,Location,Status,AssignedTech,OpenedDate,ETA,SlaDeadline,SlaBreached,AgeDays,Summary,NoteCount");
|
||||||
foreach (var t in model.Tiles)
|
foreach (var t in model.Tiles)
|
||||||
{
|
{
|
||||||
|
var etaStr = t.EstimatedCompletion.HasValue ? t.EstimatedCompletion.Value.ToString("yyyy-MM-dd") : "";
|
||||||
|
var slaStr = t.SlaDeadline.HasValue ? t.SlaDeadline.Value.ToString("yyyy-MM-dd HH:mm") : "";
|
||||||
sb.AppendLine(string.Join(",",
|
sb.AppendLine(string.Join(",",
|
||||||
t.JobId, Csv(t.DeviceSerialNumber), Csv(t.UserDisplayName),
|
t.JobId, Csv(t.DeviceSerialNumber), Csv(t.UserDisplayName),
|
||||||
Csv(t.PriorityName), Csv(t.LocationName), Csv(t.StatusOverride),
|
Csv(t.PriorityName), Csv(t.LocationName), Csv(t.StatusOverride),
|
||||||
Csv(t.AssignedTechName), t.OpenedDate.ToString("yyyy-MM-dd"),
|
Csv(t.AssignedTechName), t.OpenedDate.ToString("yyyy-MM-dd"),
|
||||||
t.EstimatedCompletion?.ToString("yyyy-MM-dd") ?? "",
|
etaStr, slaStr, t.IsSlaBreached, t.AgeDays, Csv(t.Summary), t.NoteCount));
|
||||||
t.SlaDeadline?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
|
||||||
t.IsSlaBreached, t.AgeDays, Csv(t.Summary), t.NoteCount));
|
|
||||||
}
|
}
|
||||||
var fileName = "ServiceTracker_Export_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv";
|
var fileName = "ServiceTracker_Export_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv";
|
||||||
HostController.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
HostController.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||||
return new ContentResult { Content = sb.ToString(), ContentType = "text/csv", ContentEncoding = Encoding.UTF8 };
|
return new ContentResult { Content = sb.ToString(), ContentType = "text/csv", ContentEncoding = Encoding.UTF8 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
private string GetCurrentUser()
|
||||||
|
{
|
||||||
|
if (HostController.HttpContext.User != null && HostController.HttpContext.User.Identity != null)
|
||||||
|
return HostController.HttpContext.User.Identity.Name ?? "system";
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
private ActionResult HtmlResult(string html)
|
private ActionResult HtmlResult(string html)
|
||||||
{
|
{
|
||||||
return new ContentResult { Content = html, ContentType = "text/html", ContentEncoding = Encoding.UTF8 };
|
return new ContentResult { Content = html, ContentType = "text/html", ContentEncoding = Encoding.UTF8 };
|
||||||
@@ -147,15 +150,50 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
private string Csv(string v) { return "\"" + (v ?? "").Replace("\"", "\"\"") + "\""; }
|
private string Csv(string v) { return "\"" + (v ?? "").Replace("\"", "\"\"") + "\""; }
|
||||||
private string H(string v) { return string.IsNullOrEmpty(v) ? "" : HttpUtility.HtmlEncode(v); }
|
private string H(string v) { return string.IsNullOrEmpty(v) ? "" : HttpUtility.HtmlEncode(v); }
|
||||||
|
|
||||||
|
// --- Safe accessors for job navigation properties (C#5 compatible) ---
|
||||||
|
private string SafeDeviceDomainId(Disco.Models.Repository.Job job)
|
||||||
|
{
|
||||||
|
return job.Device != null ? job.Device.DeviceDomainId : null;
|
||||||
|
}
|
||||||
|
private string SafeDeviceModelDesc(Disco.Models.Repository.Job job)
|
||||||
|
{
|
||||||
|
return (job.Device != null && job.Device.DeviceModel != null) ? job.Device.DeviceModel.Description : null;
|
||||||
|
}
|
||||||
|
private string SafeUserDisplay(Disco.Models.Repository.Job job)
|
||||||
|
{
|
||||||
|
return job.User != null ? job.User.DisplayName : job.UserId;
|
||||||
|
}
|
||||||
|
private string SafeJobTypeDesc(Disco.Models.Repository.Job job)
|
||||||
|
{
|
||||||
|
return job.JobType != null ? job.JobType.Description : job.JobTypeId;
|
||||||
|
}
|
||||||
|
private string SafeTechDisplay(Disco.Models.Repository.Job job)
|
||||||
|
{
|
||||||
|
return job.OpenedTechUser != null ? job.OpenedTechUser.DisplayName : job.OpenedTechUserId;
|
||||||
|
}
|
||||||
|
private string SafeTicketStr(ServiceTicket ticket, string field)
|
||||||
|
{
|
||||||
|
if (ticket == null) return null;
|
||||||
|
switch (field)
|
||||||
|
{
|
||||||
|
case "PriorityId": return ticket.PriorityId;
|
||||||
|
case "LocationId": return ticket.LocationId;
|
||||||
|
case "AssignedTechId": return ticket.AssignedTechId;
|
||||||
|
case "StatusOverride": return ticket.StatusOverride;
|
||||||
|
case "Summary": return ticket.Summary;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HTML Builders ---
|
||||||
|
|
||||||
private string BuildDashboardPage(DashboardViewModel model)
|
private string BuildDashboardPage(DashboardViewModel model)
|
||||||
{
|
{
|
||||||
var pluginUrl = "/Plugin/Disco.Plugins.ServiceTracker";
|
var pluginUrl = "/Plugin/Disco.Plugins.ServiceTracker";
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("<!DOCTYPE html><html><head><meta charset='utf-8'/>");
|
sb.Append("<!DOCTYPE html><html><head><meta charset='utf-8'/>");
|
||||||
sb.Append("<title>Service Tracker Dashboard</title>");
|
sb.Append("<title>Service Tracker Dashboard</title>");
|
||||||
sb.Append("<style>");
|
sb.Append("<style>"); sb.Append(GetDashboardCSS()); sb.Append("</style></head><body>");
|
||||||
sb.Append(GetDashboardCSS());
|
|
||||||
sb.Append("</style></head><body>");
|
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
sb.Append("<div class='header'><div class='header-left'>");
|
sb.Append("<div class='header'><div class='header-left'>");
|
||||||
@@ -221,10 +259,14 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
// Tile Grid
|
// Tile Grid
|
||||||
sb.Append("<div class='tile-grid'>");
|
sb.Append("<div class='tile-grid'>");
|
||||||
if (model.Tiles.Count == 0)
|
if (model.Tiles.Count == 0)
|
||||||
|
{
|
||||||
sb.Append("<div class='empty-state'><div class='empty-icon'>✅</div><div class='empty-msg'>No open jobs found</div></div>");
|
sb.Append("<div class='empty-state'><div class='empty-icon'>✅</div><div class='empty-msg'>No open jobs found</div></div>");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
foreach (var tile in model.Tiles)
|
foreach (var tile in model.Tiles)
|
||||||
sb.Append(BuildTileHtml(tile, pluginUrl));
|
sb.Append(BuildTileHtml(tile, pluginUrl));
|
||||||
|
}
|
||||||
sb.Append("</div>");
|
sb.Append("</div>");
|
||||||
|
|
||||||
// Tech Workload
|
// Tech Workload
|
||||||
@@ -314,13 +356,14 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
sb.Append("<h1>Job #" + job.Id + "</h1></div></div>");
|
sb.Append("<h1>Job #" + job.Id + "</h1></div></div>");
|
||||||
sb.Append("<div class='detail-grid'>");
|
sb.Append("<div class='detail-grid'>");
|
||||||
|
|
||||||
// Left column
|
// Left column - Job info
|
||||||
sb.Append("<div class='detail-left'><div class='detail-card'><h3>Job Details</h3><table class='detail-table'>");
|
sb.Append("<div class='detail-left'><div class='detail-card'><h3>Job Details</h3><table class='detail-table'>");
|
||||||
sb.Append("<tr><th>Device</th><td>" + H(job.DeviceSerialNumber) + (job.Device?.DeviceDomainId != null ? " (" + H(job.Device.DeviceDomainId) + ")" : "") + "</td></tr>");
|
var domainId = SafeDeviceDomainId(job);
|
||||||
sb.Append("<tr><th>Model</th><td>" + H(job.Device?.DeviceModel?.Description) + "</td></tr>");
|
sb.Append("<tr><th>Device</th><td>" + H(job.DeviceSerialNumber) + (domainId != null ? " (" + H(domainId) + ")" : "") + "</td></tr>");
|
||||||
sb.Append("<tr><th>User</th><td>" + H(job.User?.DisplayName ?? job.UserId) + "</td></tr>");
|
sb.Append("<tr><th>Model</th><td>" + H(SafeDeviceModelDesc(job)) + "</td></tr>");
|
||||||
sb.Append("<tr><th>Type</th><td>" + H(job.JobType?.Description ?? job.JobTypeId) + "</td></tr>");
|
sb.Append("<tr><th>User</th><td>" + H(SafeUserDisplay(job)) + "</td></tr>");
|
||||||
sb.Append("<tr><th>Opened</th><td>" + job.OpenedDate.ToString("dd MMM yyyy HH:mm") + " by " + H(job.OpenedTechUser?.DisplayName ?? job.OpenedTechUserId) + "</td></tr>");
|
sb.Append("<tr><th>Type</th><td>" + H(SafeJobTypeDesc(job)) + "</td></tr>");
|
||||||
|
sb.Append("<tr><th>Opened</th><td>" + job.OpenedDate.ToString("dd MMM yyyy HH:mm") + " by " + H(SafeTechDisplay(job)) + "</td></tr>");
|
||||||
if (job.ExpectedClosedDate.HasValue)
|
if (job.ExpectedClosedDate.HasValue)
|
||||||
sb.Append("<tr><th>Expected Close</th><td>" + job.ExpectedClosedDate.Value.ToString("dd MMM yyyy") + "</td></tr>");
|
sb.Append("<tr><th>Expected Close</th><td>" + job.ExpectedClosedDate.Value.ToString("dd MMM yyyy") + "</td></tr>");
|
||||||
if (job.DeviceHeld.HasValue)
|
if (job.DeviceHeld.HasValue)
|
||||||
@@ -328,33 +371,44 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
sb.Append("</table></div>");
|
sb.Append("</table></div>");
|
||||||
|
|
||||||
// Edit form
|
// Edit form
|
||||||
|
var ticketPriority = SafeTicketStr(ticket, "PriorityId") ?? config.DefaultPriorityId;
|
||||||
|
var ticketLocation = SafeTicketStr(ticket, "LocationId") ?? config.DefaultLocationId;
|
||||||
|
var ticketStatus = SafeTicketStr(ticket, "StatusOverride");
|
||||||
|
var ticketTech = SafeTicketStr(ticket, "AssignedTechId") ?? "";
|
||||||
|
var ticketSummary = SafeTicketStr(ticket, "Summary") ?? "";
|
||||||
|
var ticketEta = (ticket != null && ticket.EstimatedCompletion.HasValue) ? ticket.EstimatedCompletion.Value.ToString("yyyy-MM-dd") : "";
|
||||||
|
|
||||||
sb.Append("<div class='detail-card'><h3>Service Tracker Settings</h3>");
|
sb.Append("<div class='detail-card'><h3>Service Tracker Settings</h3>");
|
||||||
sb.Append("<form method='POST' action='" + pluginUrl + "/Update'>");
|
sb.Append("<form method='POST' action='" + pluginUrl + "/Update'>");
|
||||||
sb.Append("<input type='hidden' name='jobId' value='" + job.Id + "'/>");
|
sb.Append("<input type='hidden' name='jobId' value='" + job.Id + "'/>");
|
||||||
|
|
||||||
sb.Append("<div class='form-group'><label>Priority</label><select name='priority' class='form-control'>");
|
sb.Append("<div class='form-group'><label>Priority</label><select name='priority' class='form-control'>");
|
||||||
foreach (var p in config.Priorities)
|
foreach (var p in config.Priorities)
|
||||||
{
|
{
|
||||||
var sel = (ticket?.PriorityId ?? config.DefaultPriorityId) == p.Id ? " selected" : "";
|
var sel = ticketPriority == p.Id ? " selected" : "";
|
||||||
sb.Append("<option value='" + p.Id + "'" + sel + ">" + H(p.Name) + " (" + p.SlaHours + "h SLA)</option>");
|
sb.Append("<option value='" + p.Id + "'" + sel + ">" + H(p.Name) + " (" + p.SlaHours + "h SLA)</option>");
|
||||||
}
|
}
|
||||||
sb.Append("</select></div>");
|
sb.Append("</select></div>");
|
||||||
|
|
||||||
sb.Append("<div class='form-group'><label>Location</label><select name='location' class='form-control'>");
|
sb.Append("<div class='form-group'><label>Location</label><select name='location' class='form-control'>");
|
||||||
foreach (var l in config.Locations)
|
foreach (var l in config.Locations)
|
||||||
{
|
{
|
||||||
var sel = (ticket?.LocationId ?? config.DefaultLocationId) == l.Id ? " selected" : "";
|
var sel = ticketLocation == l.Id ? " selected" : "";
|
||||||
sb.Append("<option value='" + l.Id + "'" + sel + ">" + l.Icon + " " + H(l.Name) + "</option>");
|
sb.Append("<option value='" + l.Id + "'" + sel + ">" + l.Icon + " " + H(l.Name) + "</option>");
|
||||||
}
|
}
|
||||||
sb.Append("</select></div>");
|
sb.Append("</select></div>");
|
||||||
|
|
||||||
sb.Append("<div class='form-group'><label>Status</label><select name='status' class='form-control'><option value=''>— Use Disco Status —</option>");
|
sb.Append("<div class='form-group'><label>Status</label><select name='status' class='form-control'><option value=''>— Use Disco Status —</option>");
|
||||||
foreach (var s in config.StatusOptions)
|
foreach (var s in config.StatusOptions)
|
||||||
{
|
{
|
||||||
var sel = ticket?.StatusOverride == s ? " selected" : "";
|
var sel = ticketStatus == s ? " selected" : "";
|
||||||
sb.Append("<option value='" + H(s) + "'" + sel + ">" + H(s) + "</option>");
|
sb.Append("<option value='" + H(s) + "'" + sel + ">" + H(s) + "</option>");
|
||||||
}
|
}
|
||||||
sb.Append("</select></div>");
|
sb.Append("</select></div>");
|
||||||
sb.Append("<div class='form-group'><label>Assigned Tech (User ID)</label><input type='text' name='tech' class='form-control' value='" + H(ticket?.AssignedTechId) + "' placeholder='e.g. DOMAIN\\username'/></div>");
|
|
||||||
sb.Append("<div class='form-group'><label>ETA</label><input type='date' name='eta' class='form-control' value='" + (ticket?.EstimatedCompletion?.ToString("yyyy-MM-dd") ?? "") + "'/></div>");
|
sb.Append("<div class='form-group'><label>Assigned Tech (User ID)</label><input type='text' name='tech' class='form-control' value='" + H(ticketTech) + "' placeholder='e.g. DOMAIN\\username'/></div>");
|
||||||
sb.Append("<div class='form-group'><label>Summary</label><textarea name='summary' class='form-control' rows='3' placeholder='Brief description of the issue...'>" + H(ticket?.Summary) + "</textarea></div>");
|
sb.Append("<div class='form-group'><label>ETA</label><input type='date' name='eta' class='form-control' value='" + ticketEta + "'/></div>");
|
||||||
|
sb.Append("<div class='form-group'><label>Summary</label><textarea name='summary' class='form-control' rows='3' placeholder='Brief description of the issue...'>" + H(ticketSummary) + "</textarea></div>");
|
||||||
sb.Append("<button type='submit' class='btn btn-primary'>✔ Save Changes</button>");
|
sb.Append("<button type='submit' class='btn btn-primary'>✔ Save Changes</button>");
|
||||||
sb.Append("</form></div></div>");
|
sb.Append("</form></div></div>");
|
||||||
|
|
||||||
@@ -367,15 +421,22 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
sb.Append("<option value='general'>General</option><option value='update'>Update</option>");
|
sb.Append("<option value='general'>General</option><option value='update'>Update</option>");
|
||||||
sb.Append("<option value='escalation'>Escalation</option><option value='resolution'>Resolution</option>");
|
sb.Append("<option value='escalation'>Escalation</option><option value='resolution'>Resolution</option>");
|
||||||
sb.Append("</select><button type='submit' class='btn btn-primary btn-sm'>Add Note</button></div></form>");
|
sb.Append("</select><button type='submit' class='btn btn-primary btn-sm'>Add Note</button></div></form>");
|
||||||
if (ticket?.Notes != null && ticket.Notes.Count > 0)
|
|
||||||
|
if (ticket != null && ticket.Notes != null && ticket.Notes.Count > 0)
|
||||||
{
|
{
|
||||||
sb.Append("<div class='timeline'>");
|
sb.Append("<div class='timeline'>");
|
||||||
foreach (var note in ticket.Notes.OrderByDescending(n => n.Timestamp))
|
foreach (var note in ticket.Notes.OrderByDescending(n => n.Timestamp))
|
||||||
{
|
{
|
||||||
string tc = "#337AB7";
|
string tc = "#337AB7";
|
||||||
switch (note.NoteType) { case "escalation": tc = "#DC3545"; break; case "resolution": tc = "#28A745"; break; case "update": tc = "#FFC107"; break; }
|
switch (note.NoteType)
|
||||||
|
{
|
||||||
|
case "escalation": tc = "#DC3545"; break;
|
||||||
|
case "resolution": tc = "#28A745"; break;
|
||||||
|
case "update": tc = "#FFC107"; break;
|
||||||
|
}
|
||||||
|
var authorDisplay = note.AuthorName != null ? note.AuthorName : note.AuthorId;
|
||||||
sb.Append("<div class='timeline-item'><div class='timeline-dot' style='background:" + tc + ";'></div><div class='timeline-content'>");
|
sb.Append("<div class='timeline-item'><div class='timeline-dot' style='background:" + tc + ";'></div><div class='timeline-content'>");
|
||||||
sb.Append("<div class='timeline-header'><span class='timeline-author'>" + H(note.AuthorName ?? note.AuthorId) + "</span>");
|
sb.Append("<div class='timeline-header'><span class='timeline-author'>" + H(authorDisplay) + "</span>");
|
||||||
sb.Append("<span class='timeline-type' style='color:" + tc + ";'>" + H(note.NoteType) + "</span>");
|
sb.Append("<span class='timeline-type' style='color:" + tc + ";'>" + H(note.NoteType) + "</span>");
|
||||||
sb.Append("<span class='timeline-date'>" + note.Timestamp.ToString("dd MMM HH:mm") + "</span></div>");
|
sb.Append("<span class='timeline-date'>" + note.Timestamp.ToString("dd MMM HH:mm") + "</span></div>");
|
||||||
sb.Append("<div class='timeline-body'>" + H(note.Content) + "</div></div></div>");
|
sb.Append("<div class='timeline-body'>" + H(note.Content) + "</div></div></div>");
|
||||||
@@ -383,7 +444,9 @@ namespace Disco.Plugins.ServiceTracker.WebHandler
|
|||||||
sb.Append("</div>");
|
sb.Append("</div>");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
sb.Append("<p class='muted'>No notes yet.</p>");
|
sb.Append("<p class='muted'>No notes yet.</p>");
|
||||||
|
}
|
||||||
sb.Append("</div></div></div>");
|
sb.Append("</div></div></div>");
|
||||||
sb.Append("</body></html>");
|
sb.Append("</body></html>");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
|
|||||||
Reference in New Issue
Block a user