diff --git a/Application/Application.csproj b/Application/Application.csproj index 400e4200f..ed3e9d87c 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -21,6 +21,7 @@ IW4MAdmin.Application false + enable diff --git a/Application/Main.cs b/Application/Main.cs index 4971fa1fd..a807681c6 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -30,6 +30,7 @@ using Integrations.Source.Extensions; using IW4MAdmin.Application.Alerts; using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Localization; +using IW4MAdmin.Application.QueryHelpers; using Microsoft.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; using IW4MAdmin.Plugins.Stats.Client.Abstractions; @@ -38,6 +39,7 @@ using Stats.Client.Abstractions; using Stats.Client; using Stats.Config; using Stats.Helpers; +using WebfrontCore.QueryHelpers.Models; namespace IW4MAdmin.Application { @@ -431,6 +433,7 @@ namespace IW4MAdmin.Application .AddSingleton, ChatResourceQueryHelper>() .AddSingleton, ConnectionsResourceQueryHelper>() .AddSingleton, PermissionLevelChangedResourceQueryHelper>() + .AddSingleton, ClientResourceQueryHelper>() .AddTransient() .AddSingleton() .AddSingleton() diff --git a/Application/Misc/GeoLocationService.cs b/Application/Misc/GeoLocationService.cs index 62f586745..147380235 100644 --- a/Application/Misc/GeoLocationService.cs +++ b/Application/Misc/GeoLocationService.cs @@ -22,7 +22,7 @@ public class GeoLocationService : IGeoLocationService try { using var reader = new DatabaseReader(_sourceAddress); - reader.TryCountry(address, out country); + country = reader.Country(address); } catch { diff --git a/Application/QueryHelpers/ClientResourceQueryHelper.cs b/Application/QueryHelpers/ClientResourceQueryHelper.cs new file mode 100644 index 000000000..44df86d9c --- /dev/null +++ b/Application/QueryHelpers/ClientResourceQueryHelper.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Data.Abstractions; +using Data.Models; +using Microsoft.EntityFrameworkCore; +using SharedLibraryCore; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Helpers; +using SharedLibraryCore.Interfaces; +using WebfrontCore.QueryHelpers.Models; +using EFClient = Data.Models.Client.EFClient; + +namespace IW4MAdmin.Application.QueryHelpers; + +public class ClientResourceQueryHelper : IResourceQueryHelper +{ + private readonly IDatabaseContextFactory _contextFactory; + private readonly IGeoLocationService _geoLocationService; + + private class ClientAlias + { + public EFClient Client { get; set; } + public EFAlias Alias { get; set; } + } + + public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService) + { + _contextFactory = contextFactory; + _geoLocationService = geoLocationService; + } + + public async Task> QueryResource(ClientResourceRequest query) + { + await using var context = _contextFactory.CreateContext(false); + var iqAliases = context.Aliases.AsQueryable(); + var iqClients = context.Clients.AsQueryable(); + + var iqClientAliases = iqClients.Join(iqAliases, client => client.AliasLinkId, alias => alias.LinkId, + (client, alias) => new ClientAlias { Client = client, Alias = alias }); + + return await StartFromClient(query, iqClientAliases, iqClients); + } + + private async Task> StartFromClient(ClientResourceRequest query, + IQueryable clientAliases, IQueryable iqClients) + { + if (!string.IsNullOrWhiteSpace(query.ClientGuid)) + { + clientAliases = SearchByGuid(query, clientAliases); + } + + if (query.ClientLevel is not null) + { + clientAliases = SearchByLevel(query, clientAliases); + } + + if (query.ClientConnected is not null) + { + clientAliases = SearchByLastConnection(query, clientAliases); + } + + if (query.GameName is not null) + { + clientAliases = SearchByGame(query, clientAliases); + } + + if (!string.IsNullOrWhiteSpace(query.ClientName)) + { + clientAliases = SearchByName(query, clientAliases); + } + + if (!string.IsNullOrWhiteSpace(query.ClientIp)) + { + clientAliases = SearchByIp(query, clientAliases); + } + + var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection }); + + iqGroupedClientAliases = query.Direction == SortDirection.Descending + ? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection) + : iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection); + + var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId) + .Skip(query.Offset) + .Take(query.Count); + + // this pulls in more records than we need, but it's more efficient than ordering grouped entities + var clientLookups = await clientAliases + .Where(clientAlias => clientIds.Contains(clientAlias.Client.ClientId)) + .Select(clientAlias => new ClientResourceResponse + { + ClientId = clientAlias.Client.ClientId, + AliasId = clientAlias.Alias.AliasId, + LinkId = clientAlias.Client.AliasLinkId, + CurrentClientName = clientAlias.Client.CurrentAlias.Name, + MatchedClientName = clientAlias.Alias.Name, + CurrentClientIp = clientAlias.Client.CurrentAlias.IPAddress, + MatchedClientIp = clientAlias.Alias.IPAddress, + ClientLevel = clientAlias.Client.Level.ToLocalizedLevelName(), + ClientLevelValue = clientAlias.Client.Level, + LastConnection = clientAlias.Client.LastConnection, + Game = clientAlias.Client.GameName + }) + .ToListAsync(); + + var groupClients = clientLookups.GroupBy(x => x.ClientId); + + var orderedClients = query.Direction == SortDirection.Descending + ? groupClients.OrderByDescending(SearchByAliasLocal(query.ClientName, query.ClientIp)) + : groupClients.OrderBy(SearchByAliasLocal(query.ClientName, query.ClientIp)); + + var clients = orderedClients.Select(client => client.First()).ToList(); + await ProcessAliases(query, clients); + + return new ResourceQueryHelperResult + { + Results = clients + }; + } + + private async Task ProcessAliases(ClientResourceRequest query, IEnumerable clients) + { + await Parallel.ForEachAsync(clients, new ParallelOptions { MaxDegreeOfParallelism = 15 }, + async (client, token) => + { + if (!query.IncludeGeolocationData || client.CurrentClientIp is null) + { + return; + } + + var geolocationData = await _geoLocationService.Locate(client.CurrentClientIp.ConvertIPtoString()); + client.ClientCountryCode = geolocationData.CountryCode; + + if (!string.IsNullOrWhiteSpace(client.ClientCountryCode)) + { + client.ClientCountryDisplayName = geolocationData.Country; + } + }); + } + + private static Func, DateTime> SearchByAliasLocal(string? clientName, + string? ipAddress) + { + return group => + { + ClientResourceResponse? match = null; + var lowercaseClientName = clientName?.ToLower(); + + if (!string.IsNullOrWhiteSpace(lowercaseClientName)) + { + match = group.ToList().FirstOrDefault(SearchByNameLocal(lowercaseClientName)); + } + + if (match is null && !string.IsNullOrWhiteSpace(ipAddress)) + { + match = group.ToList().FirstOrDefault(SearchByIpLocal(ipAddress)); + } + + return (match ?? group.First()).LastConnection; + }; + } + + private static Func SearchByNameLocal(string clientName) + { + return clientResourceResponse => + clientResourceResponse.MatchedClientName.Contains(clientName); + } + + private static Func SearchByIpLocal(string clientIp) + { + return clientResourceResponse => clientResourceResponse.MatchedClientIp.ConvertIPtoString().Contains(clientIp); + } + + private static IQueryable SearchByName(ClientResourceRequest query, + IQueryable clientAliases) + { + var lowerCaseQueryName = query.ClientName.ToLower(); + + clientAliases = clientAliases.Where(query.IsExactClientName + ? ExactNameMatch(lowerCaseQueryName) + : LikeNameMatch(lowerCaseQueryName)); + + return clientAliases; + } + + private static Expression> LikeNameMatch(string lowerCaseQueryName) + { + return clientAlias => EF.Functions.Like( + clientAlias.Alias.SearchableName, + $"%{lowerCaseQueryName}%") || EF.Functions.Like( + clientAlias.Alias.Name.ToLower(), + $"%{lowerCaseQueryName}%"); + } + + private static Expression> ExactNameMatch(string lowerCaseQueryName) + { + return clientAlias => + lowerCaseQueryName == clientAlias.Alias.Name || lowerCaseQueryName == clientAlias.Alias.SearchableName; + } + + private static IQueryable SearchByIp(ClientResourceRequest query, + IQueryable clientAliases) + { + var ipString = query.ClientIp.Trim(); + var ipAddress = ipString.ConvertToIP(); + + if (ipAddress != null && ipString.Split('.').Length == 4 && query.IsExactClientIp) + { + clientAliases = clientAliases.Where(clientAlias => + clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress); + } + else + { + clientAliases = clientAliases.Where(clientAlias => + EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%")); + } + + return clientAliases; + } + + private static IQueryable SearchByGuid(ClientResourceRequest query, + IQueryable clients) + { + var guidString = query.ClientGuid.Trim(); + var parsedGuids = new List(); + long guid = 0; + + try + { + guid = guidString.ConvertGuidToLong(NumberStyles.HexNumber, false, 0); + } + catch + { + // ignored + } + + if (guid != 0) + { + parsedGuids.Add(guid); + } + + try + { + guid = guidString.ConvertGuidToLong(NumberStyles.Integer, false, 0); + } + catch + { + // ignored + } + + if (guid != 0) + { + parsedGuids.Add(guid); + } + + if (!parsedGuids.Any()) + { + return clients; + } + + clients = clients.Where(client => parsedGuids.Contains(client.Client.NetworkId)); + return clients; + } + + private static IQueryable SearchByLevel(ClientResourceRequest query, IQueryable clients) + { + clients = clients.Where(clientAlias => clientAlias.Client.Level == query.ClientLevel); + + return clients; + } + + private static IQueryable SearchByLastConnection(ClientResourceRequest query, + IQueryable clients) + { + clients = clients.Where(clientAlias => clientAlias.Client.LastConnection >= query.ClientConnected); + + return clients; + } + + private static IQueryable SearchByGame(ClientResourceRequest query, IQueryable clients) + { + clients = clients.Where(clientAlias => clientAlias.Client.GameName == query.GameName); + + return clients; + } +} diff --git a/SharedLibraryCore/Dtos/PaginationRequest.cs b/SharedLibraryCore/Dtos/PaginationRequest.cs index d9a7312a9..45a29d582 100644 --- a/SharedLibraryCore/Dtos/PaginationRequest.cs +++ b/SharedLibraryCore/Dtos/PaginationRequest.cs @@ -15,7 +15,7 @@ namespace SharedLibraryCore.Dtos /// /// how many items to take /// - public int Count { get; set; } = 100; + public int Count { get; set; } = 30; /// /// filter query @@ -28,6 +28,8 @@ namespace SharedLibraryCore.Dtos public SortDirection Direction { get; set; } = SortDirection.Descending; public DateTime? Before { get; set; } + + public DateTime? After { get; set; } } public enum SortDirection diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index b76bb1807..b7d8a11be 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -26,6 +26,7 @@ using static SharedLibraryCore.Server; using static Data.Models.Client.EFClient; using static Data.Models.EFPenalty; using ILogger = Microsoft.Extensions.Logging.ILogger; +using RegionInfo = System.Globalization.RegionInfo; namespace SharedLibraryCore { @@ -316,14 +317,20 @@ namespace SharedLibraryCore } } + public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null) + { + return ConvertGuidToLong(str, numberStyle, true, fallback); + } + /// /// converts a string to numerical guid /// /// source string for guid /// how to parse the guid /// value to use if string is empty + /// convert signed values to unsigned /// - public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null) + public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, bool convertSigned, long? fallback = null) { // added for source games that provide the steam ID var match = Regex.Match(str, @"^STEAM_(\d):(\d):(\d+)$"); @@ -336,7 +343,7 @@ namespace SharedLibraryCore return z * 2 + 0x0110000100000000 + y; } - str = str.Substring(0, Math.Min(str.Length, 19)); + str = str.Substring(0, Math.Min(str.Length, str.StartsWith("-") ? 20 : 19)); var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value; if (string.IsNullOrWhiteSpace(str) && fallback.HasValue) @@ -351,7 +358,7 @@ namespace SharedLibraryCore { long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id); - if (id < 0) + if (id < 0 && convertSigned) { id = (uint)id; } diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index dad7b354f..338e62fae 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -862,7 +862,6 @@ namespace WebfrontCore.Controllers private Dictionary GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values .Concat(_appConfig.GlobalRules) .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty())) - .Distinct() .Select((value, _) => new { Value = value @@ -872,6 +871,7 @@ namespace WebfrontCore.Controllers { Value = "" }) + .Distinct() .ToDictionary(item => item.Value, item => item.Value); } } diff --git a/WebfrontCore/Controllers/Client/ClientController.cs b/WebfrontCore/Controllers/Client/ClientController.cs index 7590b386a..9ec2c49ff 100644 --- a/WebfrontCore/Controllers/Client/ClientController.cs +++ b/WebfrontCore/Controllers/Client/ClientController.cs @@ -15,6 +15,7 @@ using Data.Models; using SharedLibraryCore.Services; using Stats.Config; using WebfrontCore.Permissions; +using WebfrontCore.QueryHelpers.Models; using WebfrontCore.ViewComponents; namespace WebfrontCore.Controllers @@ -26,15 +27,19 @@ namespace WebfrontCore.Controllers private readonly IGeoLocationService _geoLocationService; private readonly ClientService _clientService; private readonly IInteractionRegistration _interactionRegistration; + private readonly IResourceQueryHelper _clientResourceHelper; public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config, - IGeoLocationService geoLocationService, ClientService clientService, IInteractionRegistration interactionRegistration) : base(manager) + IGeoLocationService geoLocationService, ClientService clientService, + IInteractionRegistration interactionRegistration, + IResourceQueryHelper clientResourceHelper) : base(manager) { _metaService = metaService; _config = config; _geoLocationService = geoLocationService; _clientService = clientService; _interactionRegistration = interactionRegistration; + _clientResourceHelper = clientResourceHelper; } [Obsolete] @@ -241,6 +246,17 @@ namespace WebfrontCore.Controllers return View("Find/Index", clientsDto); } + public async Task AdvancedFind(ClientResourceRequest request) + { + ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"]; + ViewBag.ClientResourceRequest = request; + + var response = await _clientResourceHelper.QueryResource(request); + return request.Offset > 0 + ? PartialView("Find/_AdvancedFindList", response.Results) + : View("Find/AdvancedFind", response.Results); + } + public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType, CancellationToken token) { diff --git a/WebfrontCore/Permissions/WebfrontEntity.cs b/WebfrontCore/Permissions/WebfrontEntity.cs index 39f615f09..91869da4c 100644 --- a/WebfrontCore/Permissions/WebfrontEntity.cs +++ b/WebfrontCore/Permissions/WebfrontEntity.cs @@ -16,7 +16,8 @@ public enum WebfrontEntity ProfilePage, AdminMenu, ClientNote, - Interaction + Interaction, + AdvancedSearch } public enum WebfrontPermission diff --git a/WebfrontCore/QueryHelpers/Models/ClientResourceRequest.cs b/WebfrontCore/QueryHelpers/Models/ClientResourceRequest.cs new file mode 100644 index 000000000..eeb71da97 --- /dev/null +++ b/WebfrontCore/QueryHelpers/Models/ClientResourceRequest.cs @@ -0,0 +1,19 @@ +using System; +using Data.Models; +using Data.Models.Client; +using SharedLibraryCore.QueryHelper; + +namespace WebfrontCore.QueryHelpers.Models; + +public class ClientResourceRequest : ClientPaginationRequest +{ + public string ClientName { get; set; } + public bool IsExactClientName { get; set; } + public string ClientIp { get; set; } + public bool IsExactClientIp { get; set; } + public string ClientGuid { get; set; } + public DateTime? ClientConnected { get; set; } + public EFClient.Permission? ClientLevel { get; set; } + public Reference.Game? GameName { get; set; } + public bool IncludeGeolocationData { get; set; } = true; +} diff --git a/WebfrontCore/QueryHelpers/Models/ClientResourceResponse.cs b/WebfrontCore/QueryHelpers/Models/ClientResourceResponse.cs new file mode 100644 index 000000000..d4cab44cd --- /dev/null +++ b/WebfrontCore/QueryHelpers/Models/ClientResourceResponse.cs @@ -0,0 +1,22 @@ +using System; +using Data.Models; +using Data.Models.Client; + +namespace WebfrontCore.QueryHelpers.Models; + +public class ClientResourceResponse +{ + public int ClientId { get; set; } + public int AliasId { get; set; } + public int LinkId { get; set; } + public string CurrentClientName { get; set; } + public string MatchedClientName { get; set; } + public int? CurrentClientIp { get; set; } + public int? MatchedClientIp { get; set; } + public string ClientCountryCode { get; set; } + public string ClientCountryDisplayName { get; set; } + public string ClientLevel { get; set; } + public EFClient.Permission ClientLevelValue { get; set; } + public DateTime LastConnection { get; set; } + public Reference.Game Game { get; set; } +} diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index fc49e5724..9c8e44077 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -115,6 +115,9 @@ namespace WebfrontCore services.AddSingleton, ClientService>(); services.AddSingleton, StatsResourceQueryHelper>(); services.AddSingleton, AdvancedClientStatsResourceQueryHelper>(); + services.AddScoped(sp => + Program.ApplicationServiceProvider + .GetRequiredService>()); services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>)); // todo: this needs to be handled more gracefully services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); diff --git a/WebfrontCore/Views/Client/Find/AdvancedFind.cshtml b/WebfrontCore/Views/Client/Find/AdvancedFind.cshtml new file mode 100644 index 000000000..cf66ba26f --- /dev/null +++ b/WebfrontCore/Views/Client/Find/AdvancedFind.cshtml @@ -0,0 +1,39 @@ +@model IEnumerable +@{ + var loc = Utilities.CurrentLocalization.LocalizationIndex; +} + +
+

@loc["WEBFRONT_SEARCH_RESULTS_TITLE"]

+ + + + + + + + + + + + + + + +
@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_NAME_FULL"]@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_IP"]@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_COUNTRY"]@loc["WEBFRONT_PROFILE_LEVEL"]@loc["WEBFRONT_ADVANCED_SEARCH_LABEL_GAME"]@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
+ +
+ +
+
+ +@section scripts { + +} diff --git a/WebfrontCore/Views/Client/Find/_AdvancedFindContent.cshtml b/WebfrontCore/Views/Client/Find/_AdvancedFindContent.cshtml new file mode 100644 index 000000000..cce7ecf53 --- /dev/null +++ b/WebfrontCore/Views/Client/Find/_AdvancedFindContent.cshtml @@ -0,0 +1,107 @@ +@using WebfrontCore.QueryHelpers.Models +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using WebfrontCore.Permissions +@using Data.Models.Client +@model WebfrontCore.QueryHelpers.Models.ClientResourceResponse + +@{ + var loc = Utilities.CurrentLocalization.LocalizationIndex; + var client = Model; + + string FormatNameChange(ClientResourceResponse clientResponse) + { + return clientResponse.CurrentClientName.StripColors() != clientResponse.MatchedClientName?.StripColors() + ? $"{clientResponse.CurrentClientName} [{clientResponse.MatchedClientName}{(clientResponse.MatchedClientIp is null && clientResponse.MatchedClientIp != clientResponse.CurrentClientIp ? "" : $"/{clientResponse.MatchedClientIp.ConvertIPtoString()}")}]" + : clientResponse.CurrentClientName; + } + + var canSeeLevel = (ViewBag.PermissionsSet as IEnumerable).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read); + var canSeeIp = (ViewBag.PermissionsSet as IEnumerable).HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read); + string ClassForLevel(EFClient.Permission permission) => !canSeeLevel ? "level-color-user" : $"level-color-{permission.ToString().ToLower()}"; + string FormatIpForPermission(int? ip) => canSeeIp && ip is not null ? ip.ConvertIPtoString() : "-"; +} + + + + + + + + + @FormatIpForPermission(client.CurrentClientIp) + + +
+ @if (string.IsNullOrEmpty(client.ClientCountryCode)) + { +
+ +
Unknown
+
+ } + else + { + @client.ClientCountryDisplayName + } +
@client.ClientCountryDisplayName
+
+ + @(canSeeLevel ? client.ClientLevel : "-") + +
+ @Utilities.MakeAbbreviation(ViewBag.Localization["GAME_" + client.Game]) +
+ + +
+
+ @client.LastConnection.HumanizeForCurrentCulture() +
+
+ + + + + +
@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_NAME"]
+
@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_ALIAS"]
+
@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_IP"]
+
@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_COUNTRY"]
+
@loc["WEBFRONT_PROFILE_LEVEL"]
+
@loc["WEBFRONT_ADVANCED_SEARCH_LABEL_GAME"]
+
@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
+ + +
+ + + +
+
+ + @if (client.MatchedClientIp != client.CurrentClientIp) + { + / @client.MatchedClientIp.ConvertIPtoString() + } +
+
+ @FormatIpForPermission(client.CurrentClientIp) +
+
+ @if (string.IsNullOrEmpty(client.ClientCountryCode)) + { +
Unknown
+ } + else + { +
@client.ClientCountryDisplayName
+ @client.ClientCountryDisplayName + } +
+
@(canSeeLevel ? client.ClientLevel : "-")
+
+ @Utilities.MakeAbbreviation(ViewBag.Localization["GAME_" + client.Game]) +
+
@client.LastConnection.HumanizeForCurrentCulture()
+ + diff --git a/WebfrontCore/Views/Client/Find/_AdvancedFindList.cshtml b/WebfrontCore/Views/Client/Find/_AdvancedFindList.cshtml new file mode 100644 index 000000000..1bb60794e --- /dev/null +++ b/WebfrontCore/Views/Client/Find/_AdvancedFindList.cshtml @@ -0,0 +1,6 @@ +@model IEnumerable + +@foreach (var client in Model) +{ + +} diff --git a/WebfrontCore/Views/Shared/Partials/_SearchResourceFilter.cshtml b/WebfrontCore/Views/Shared/Partials/_SearchResourceFilter.cshtml new file mode 100644 index 000000000..63a3454e8 --- /dev/null +++ b/WebfrontCore/Views/Shared/Partials/_SearchResourceFilter.cshtml @@ -0,0 +1,125 @@ +@using Data.Models.Client +@using SharedLibraryCore.Dtos +@using WebfrontCore.QueryHelpers.Models +@using Data.Models +@model string +@{ + var existingClientFilter = ViewBag.ClientResourceRequest as ClientResourceRequest; +} + + + diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index 73731668d..b4c2361cc 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -126,7 +126,7 @@
- +
@@ -158,6 +158,7 @@ + diff --git a/WebfrontCore/Views/Shared/_LeftNavBar.cshtml b/WebfrontCore/Views/Shared/_LeftNavBar.cshtml index 0d59ad896..1027fafce 100644 --- a/WebfrontCore/Views/Shared/_LeftNavBar.cshtml +++ b/WebfrontCore/Views/Shared/_LeftNavBar.cshtml @@ -9,7 +9,7 @@