From 71ce4bcde3b852d861c0c5727835ccff3cee2ce4 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Mon, 1 Jun 2026 13:04:00 -0700 Subject: [PATCH 1/2] RE1-T117 Inventory and audit fix. --- .../Areas/User/Inventory/Inventory.ar.resx | 2 + .../Areas/User/Inventory/Inventory.de.resx | 6 ++ .../Areas/User/Inventory/Inventory.en.resx | 6 ++ .../Areas/User/Inventory/Inventory.es.resx | 6 ++ .../Areas/User/Inventory/Inventory.fr.resx | 6 ++ .../Areas/User/Inventory/Inventory.it.resx | 6 ++ .../Areas/User/Inventory/Inventory.pl.resx | 6 ++ .../Areas/User/Inventory/Inventory.sv.resx | 6 ++ .../Areas/User/Inventory/Inventory.uk.resx | 6 ++ .../Providers/IAuditEventProvider.cs | 10 --- .../Services/IAuditEventService.cs | 6 -- Core/Resgrid.Services/AuditEventService.cs | 26 ------- Core/Resgrid.Services/InventoryService.cs | 12 +++- .../AuditEventProvider.cs | 33 --------- .../v4/ContactVerificationController.cs | 5 +- .../Areas/User/Controllers/HomeController.cs | 4 +- .../User/Controllers/InventoryController.cs | 70 +++++++++++++++++-- .../User/Controllers/ShiftsController.cs | 16 ++--- .../Areas/User/Views/Inventory/ByUnit.cshtml | 60 ++++++++++++++++ .../Areas/User/Views/Inventory/History.cshtml | 1 + .../Areas/User/Views/Inventory/Index.cshtml | 1 + Web/Resgrid.Web/wwwroot/_references.js | 1 + .../inventory/resgrid.inventory.byunit.js | 36 ++++++++++ .../inventory/resgrid.inventory.history.js | 3 +- .../Logic/AuditQueueLogic.cs | 20 ++++-- 25 files changed, 255 insertions(+), 99 deletions(-) delete mode 100644 Core/Resgrid.Model/Providers/IAuditEventProvider.cs delete mode 100644 Core/Resgrid.Model/Services/IAuditEventService.cs delete mode 100644 Core/Resgrid.Services/AuditEventService.cs delete mode 100644 Providers/Resgrid.Providers.Bus/AuditEventProvider.cs create mode 100644 Web/Resgrid.Web/Areas/User/Views/Inventory/ByUnit.cshtml create mode 100644 Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.byunit.js diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx index c586e122e..c7ff6f421 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx @@ -10,6 +10,7 @@ أضيف في إضافة نوع مخزون تعديل المخزون + يجب تقديم تعديل مخزون غير صفري (العدد/الكمية). الكمية الدفعة الرقم التسلسلي للدفعة @@ -18,6 +19,7 @@ تعديل نوع المخزون للعناصر التي لا تنتهي صلاحيتها المجموعة + المخزون حسب الوحدة سجل المخزون أنواع المخزون تنتهي صلاحية العنصر بعد (أيام) diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx index 7823251f0..87a0ffa4c 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx @@ -79,6 +79,9 @@ Inventar anpassen + + Sie müssen eine von null abweichende Bestandsanpassung angeben (Anzahl/Menge). + Menge @@ -112,6 +115,9 @@ Gruppe + + Inventar nach Einheit + Inventarverlauf diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx index 9e24daa7c..2487990f5 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx @@ -138,6 +138,9 @@ Adjust Inventory + + You must supply a non-zero inventory adjustment (count/amount). + Amount @@ -171,6 +174,9 @@ Group + + Inventory by Unit + Inventory History diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx index 21d93aada..d1d7195c5 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx @@ -79,6 +79,9 @@ Ajustar inventario + + Debe proporcionar un ajuste de inventario distinto de cero (cantidad). + Cantidad @@ -112,6 +115,9 @@ Grupo + + Inventario por unidad + Historial de inventario diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx index 4a775861a..9aa9b25b4 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx @@ -79,6 +79,9 @@ Ajuster l'inventaire + + Vous devez fournir un ajustement d'inventaire non nul (nombre/quantité). + Quantité @@ -112,6 +115,9 @@ Groupe + + Inventaire par unité + Historique de l'inventaire diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx index e958e7029..29a8066bb 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx @@ -79,6 +79,9 @@ Rettifica inventario + + È necessario fornire una rettifica di inventario diversa da zero (conteggio/quantità). + Quantità @@ -112,6 +115,9 @@ Gruppo + + Inventario per unità + Cronologia inventario diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx index 23744ce85..2fa89b58b 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx @@ -79,6 +79,9 @@ Dostosuj inwentarz + + Musisz podać niezerową korektę stanu magazynowego (liczba/ilość). + Ilość @@ -112,6 +115,9 @@ Grupa + + Inwentarz według jednostki + Historia inwentarza diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx index 8a60609a7..498d90fa6 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx @@ -79,6 +79,9 @@ Justera lager + + Du måste ange en lagerjustering som inte är noll (antal/mängd). + Antal @@ -112,6 +115,9 @@ Grupp + + Inventering per enhet + Lagerhistorik diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx index 1d4362a0d..7ef52a19e 100644 --- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx @@ -79,6 +79,9 @@ Коригувати інвентар + + Ви повинні вказати ненульове коригування запасів (кількість). + Кількість @@ -112,6 +115,9 @@ Група + + Інвентар за підрозділом + Історія інвентарю diff --git a/Core/Resgrid.Model/Providers/IAuditEventProvider.cs b/Core/Resgrid.Model/Providers/IAuditEventProvider.cs deleted file mode 100644 index 1f2d80b8d..000000000 --- a/Core/Resgrid.Model/Providers/IAuditEventProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Resgrid.Model.Events; -using System.Threading.Tasks; - -namespace Resgrid.Model.Providers -{ - public interface IAuditEventProvider - { - Task EnqueueAuditEventAsync(AuditEvent auditEvent); - } -} diff --git a/Core/Resgrid.Model/Services/IAuditEventService.cs b/Core/Resgrid.Model/Services/IAuditEventService.cs deleted file mode 100644 index 76d997a0f..000000000 --- a/Core/Resgrid.Model/Services/IAuditEventService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Resgrid.Model.Services -{ - public interface IAuditEventService - { - } -} diff --git a/Core/Resgrid.Services/AuditEventService.cs b/Core/Resgrid.Services/AuditEventService.cs deleted file mode 100644 index 2da4664d2..000000000 --- a/Core/Resgrid.Services/AuditEventService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Resgrid.Model.Events; -using Resgrid.Model.Services; -using Resgrid.Model.Providers; - -namespace Resgrid.Services -{ - public class AuditEventService : IAuditEventService - { - private readonly IEventAggregator _eventAggregator; - private static IAuditEventProvider _auditEventProvider; - - public AuditEventService(IEventAggregator eventAggregator, IAuditEventProvider auditEventProvider) - { - _eventAggregator = eventAggregator; - _auditEventProvider = auditEventProvider; - - _eventAggregator.AddListener(auditEventHandler); - } - - private Action auditEventHandler = async delegate(AuditEvent message) - { - await _auditEventProvider.EnqueueAuditEventAsync(message); - }; - } -} diff --git a/Core/Resgrid.Services/InventoryService.cs b/Core/Resgrid.Services/InventoryService.cs index 89e833272..fd9daab96 100644 --- a/Core/Resgrid.Services/InventoryService.cs +++ b/Core/Resgrid.Services/InventoryService.cs @@ -13,12 +13,14 @@ public class InventoryService: IInventoryService private readonly IInventoryTypesRepository _inventoryTypesRepository; private readonly IInventoryRepository _inventoryRepository; private readonly IDepartmentGroupsService _departmentGroupsService; + private readonly IUnitsService _unitsService; - public InventoryService(IInventoryTypesRepository inventoryTypesRepository, IInventoryRepository inventoryRepository, IDepartmentGroupsService departmentGroupsService) + public InventoryService(IInventoryTypesRepository inventoryTypesRepository, IInventoryRepository inventoryRepository, IDepartmentGroupsService departmentGroupsService, IUnitsService unitsService) { _inventoryTypesRepository = inventoryTypesRepository; _inventoryRepository = inventoryRepository; _departmentGroupsService = departmentGroupsService; + _unitsService = unitsService; } public async Task GetTypeByIdAsync(int typeId) @@ -38,6 +40,9 @@ public async Task GetInventoryByIdAsync(int inventoryId) if (inventory != null && inventory.GroupId > 0) inventory.Group = await _departmentGroupsService.GetGroupByIdAsync(inventory.GroupId); + if (inventory != null && inventory.UnitId.HasValue && inventory.UnitId.Value > 0) + inventory.Unit = await _unitsService.GetUnitByIdAsync(inventory.UnitId.Value); + return inventory; } @@ -65,9 +70,14 @@ public async Task> GetAllTransactionsForDepartmentAsync(int depa { var inventories = await _inventoryRepository.GetAllInventoriesByDepartmentIdAsync(departmentId); + var units = await _unitsService.GetUnitsForDepartmentAsync(departmentId); + foreach (var inventory in inventories) { inventory.Group = await _departmentGroupsService.GetGroupByIdAsync(inventory.GroupId); + + if (inventory.UnitId.HasValue && inventory.UnitId.Value > 0) + inventory.Unit = units?.FirstOrDefault(x => x.UnitId == inventory.UnitId.Value); } return inventories.ToList(); diff --git a/Providers/Resgrid.Providers.Bus/AuditEventProvider.cs b/Providers/Resgrid.Providers.Bus/AuditEventProvider.cs deleted file mode 100644 index e4aa86f89..000000000 --- a/Providers/Resgrid.Providers.Bus/AuditEventProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Resgrid.Framework; -using Resgrid.Model; -using Resgrid.Model.Events; -using Resgrid.Model.Providers; -using Resgrid.Providers.Bus.Rabbit; - -namespace Resgrid.Providers.Bus -{ - public class AuditEventProvider : IAuditEventProvider - { - private readonly RabbitOutboundQueueProvider _rabbitOutboundQueueProvider; - - public AuditEventProvider() - { - _rabbitOutboundQueueProvider = new RabbitOutboundQueueProvider(); - } - - public async Task EnqueueAuditEventAsync(AuditEvent auditEvent) - { - if (Config.SystemBehaviorConfig.ServiceBusType == Config.ServiceBusTypes.Rabbit) - { - _rabbitOutboundQueueProvider.EnqueueAuditEvent(auditEvent); - return true; - } - - return false; - } - } -} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/ContactVerificationController.cs b/Web/Resgrid.Web.Services/Controllers/v4/ContactVerificationController.cs index a58ccdfc7..943c1ce3f 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/ContactVerificationController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/ContactVerificationController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Resgrid.Model; using Resgrid.Model.Services; +using Resgrid.Web.Services.Helpers; using Resgrid.Web.Services.Models.v4.ContactVerification; namespace Resgrid.Web.Services.Controllers.v4 @@ -87,7 +88,9 @@ public async Task> ConfirmVerificati if (!ModelState.IsValid) return BadRequest(); - string ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); + // Use the X-Forwarded-For aware helper so the audit log records the real client IP + // rather than the reverse-proxy / load-balancer address. + string ipAddress = IpAddressHelper.GetRequestIP(Request, true); bool confirmed = await _contactVerificationService.ConfirmVerificationCodeAsync( UserId, DepartmentId, model.Type, model.Code, ipAddress, cancellationToken); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs b/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs index fd7cde02d..576d0c283 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs @@ -1076,7 +1076,9 @@ public async Task ConfirmContactVerificationCode([FromBody] Confi if (request == null || string.IsNullOrWhiteSpace(request.Code)) return BadRequest(); - string ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); + // Use the X-Forwarded-For aware helper so the audit log records the real client IP + // rather than the reverse-proxy / load-balancer address. + string ipAddress = IpAddressHelper.GetRequestIP(Request, true); bool confirmed = await _contactVerificationService.ConfirmVerificationCodeAsync(UserId, DepartmentId, request.Type, request.Code, ipAddress, cancellationToken); return Json(new { success = confirmed }); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/InventoryController.cs b/Web/Resgrid.Web/Areas/User/Controllers/InventoryController.cs index f5a30f4fe..a852983fc 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/InventoryController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/InventoryController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using Resgrid.Model; using Resgrid.Model.Services; using Resgrid.Web.Areas.User.Models.Inventory; @@ -21,14 +22,19 @@ public class InventoryController : SecureBaseController private readonly IUnitsService _unitsService; private readonly IDepartmentsService _departmentsService; private readonly IUserProfileService _userProfileService; + private readonly IStringLocalizer _localizer; + private readonly IStringLocalizer _commonLocalizer; - public InventoryController(IInventoryService inventoryService, IDepartmentGroupsService departmentGroupsService, IUnitsService unitsService, IDepartmentsService departmentsService, IUserProfileService userProfileService) + public InventoryController(IInventoryService inventoryService, IDepartmentGroupsService departmentGroupsService, IUnitsService unitsService, IDepartmentsService departmentsService, IUserProfileService userProfileService, + IStringLocalizer localizer, IStringLocalizer commonLocalizer) { _inventoryService = inventoryService; _departmentGroupsService = departmentGroupsService; _unitsService = unitsService; _departmentsService = departmentsService; _userProfileService = userProfileService; + _localizer = localizer; + _commonLocalizer = commonLocalizer; } #endregion Private Members and Constructors @@ -71,7 +77,7 @@ public async Task Adjust() public async Task Adjust(AdjustView model) { if (model.Inventory.Amount == 0) - ModelState.AddModelError("Inventory.Amount", "You must supply a non-zero inventory adjustment (count/amount)."); + ModelState.AddModelError("Inventory.Amount", _localizer["AdjustmentAmountRequired"]); if (ModelState.IsValid) { @@ -114,7 +120,7 @@ public async Task ViewEntry(int inventoryId) if (profile != null) model.Name = profile.FullName.AsFirstNameLastName; else - model.Name = "Unknown"; + model.Name = _commonLocalizer["Unknown"]; return View(model); } @@ -230,7 +236,7 @@ public async Task GetCombinedInventoryList() if (item.Unit != null) inventory.Unit = item.Unit.Name; else - inventory.Unit = "No Unit"; + inventory.Unit = _localizer["NoUnit"]; inventory.Count = item.Amount; @@ -264,19 +270,19 @@ public async Task GetInventoryList() if (item.Unit != null) inventory.Unit = item.Unit.Name; else - inventory.Unit = "No Unit"; + inventory.Unit = _localizer["NoUnit"]; if (item.Group != null) inventory.Group = item.Group.Name; else - inventory.Group = "No Group"; + inventory.Group = _localizer["NoGroup"]; var name = names.FirstOrDefault(x => x.UserId == item.AddedByUserId); if (name != null) inventory.UserName = name.Name; else - inventory.UserName = "Unknown"; + inventory.UserName = _commonLocalizer["Unknown"]; inventoryJson.Add(inventory); @@ -284,5 +290,55 @@ public async Task GetInventoryList() return Json(inventoryJson); } + + [Authorize(Policy = ResgridResources.Inventory_View)] + public async Task ByUnit() + { + return View(); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Inventory_View)] + public async Task GetInventoryByUnitList() + { + List inventoryJson = new List(); + + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var items = await _inventoryService.GetAllTransactionsForDepartmentAsync(DepartmentId); + var names = await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId); + + foreach (var item in items.Where(x => x.UnitId.HasValue && x.UnitId.Value > 0) + .OrderBy(x => x.Unit != null ? x.Unit.Name : string.Empty) + .ThenBy(x => x.Type != null ? x.Type.Type : string.Empty)) + { + var inventory = new InventoryJson(); + inventory.InventoryId = item.InventoryId; + inventory.Type = item.Type.Type; + inventory.Amount = item.Amount; + inventory.Batch = item.Batch; + inventory.Timestamp = item.TimeStamp.FormatForDepartment(department); + + if (item.Unit != null) + inventory.Unit = item.Unit.Name; + else + inventory.Unit = _localizer["NoUnit"]; + + if (item.Group != null) + inventory.Group = item.Group.Name; + else + inventory.Group = _localizer["NoGroup"]; + + var name = names.FirstOrDefault(x => x.UserId == item.AddedByUserId); + + if (name != null) + inventory.UserName = name.Name; + else + inventory.UserName = _commonLocalizer["Unknown"]; + + inventoryJson.Add(inventory); + } + + return Json(inventoryJson); + } } } diff --git a/Web/Resgrid.Web/Areas/User/Controllers/ShiftsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/ShiftsController.cs index 673748322..f8e144022 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/ShiftsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/ShiftsController.cs @@ -960,21 +960,21 @@ public async Task GetShiftCalendarItems() DateTime startResult; if (!String.IsNullOrWhiteSpace(shift.StartTime) && DateTime.TryParse(shift.StartTime, out startResult)) { - item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, startResult.Hour, startResult.Minute, 0, DateTimeKind.Local); + item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, startResult.Hour, startResult.Minute, 0, DateTimeKind.Unspecified); } else { - item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 0, 0, 0, DateTimeKind.Local); + item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 0, 0, 0, DateTimeKind.Unspecified); } DateTime endResult; if (!String.IsNullOrWhiteSpace(shift.EndTime) && DateTime.TryParse(shift.EndTime, out endResult)) { - item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, endResult.Hour, endResult.Minute, 0, DateTimeKind.Local); + item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, endResult.Hour, endResult.Minute, 0, DateTimeKind.Unspecified); } else { - item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 23, 59, 59, DateTimeKind.Local); + item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 23, 59, 59, DateTimeKind.Unspecified); } item.Color = shift.Color; @@ -1109,21 +1109,21 @@ public async Task GetShiftCalendarItemsForShift(int shiftId) DateTime startResult; if (!String.IsNullOrWhiteSpace(shift.StartTime) && DateTime.TryParse(shift.StartTime, out startResult)) { - item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, startResult.Hour, startResult.Minute, 0, DateTimeKind.Local); + item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, startResult.Hour, startResult.Minute, 0, DateTimeKind.Unspecified); } else { - item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 0, 0, 0, DateTimeKind.Local); + item.Start = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 0, 0, 0, DateTimeKind.Unspecified); } DateTime endResult; if (!String.IsNullOrWhiteSpace(shift.EndTime) && DateTime.TryParse(shift.EndTime, out endResult)) { - item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, endResult.Hour, endResult.Minute, 0, DateTimeKind.Local); + item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, endResult.Hour, endResult.Minute, 0, DateTimeKind.Unspecified); } else { - item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 23, 59, 59, DateTimeKind.Local); + item.End = new DateTime(day.Day.Year, day.Day.Month, day.Day.Day, 23, 59, 59, DateTimeKind.Unspecified); } item.Title = shift.Name; diff --git a/Web/Resgrid.Web/Areas/User/Views/Inventory/ByUnit.cshtml b/Web/Resgrid.Web/Areas/User/Views/Inventory/ByUnit.cshtml new file mode 100644 index 000000000..d8bb96f1d --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Inventory/ByUnit.cshtml @@ -0,0 +1,60 @@ +@using Resgrid.Model +@using Resgrid.Web.Helpers +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + localizer["InventoryByUnitHeader"]; + Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; +} + + + +
+
+

@localizer["InventoryByUnitHeader"]

+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +@section Scripts +{ + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Inventory/History.cshtml b/Web/Resgrid.Web/Areas/User/Views/Inventory/History.cshtml index 9d9c1a71d..3e74fc94c 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Inventory/History.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Inventory/History.cshtml @@ -49,6 +49,7 @@ type: '@Html.Raw(commonLocalizer["Type"].Value)', amount: '@Html.Raw(localizer["Amount"].Value)', group: '@Html.Raw(localizer["Group"].Value)', + unit: '@Html.Raw(localizer["Unit"].Value)', batch: '@Html.Raw(localizer["Batch"].Value)', timestamp: '@Html.Raw(commonLocalizer["Timestamp"].Value)', addedBy: '@Html.Raw(localizer["AddedBy"].Value)', diff --git a/Web/Resgrid.Web/Areas/User/Views/Inventory/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Inventory/Index.cshtml index 2907553e1..802e2d11f 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Inventory/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Inventory/Index.cshtml @@ -31,6 +31,7 @@ @localizer["AdjustInventory"] } @localizer["ViewHistory"] + @localizer["InventoryByUnitHeader"] @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) { @localizer["ManageTypes"] diff --git a/Web/Resgrid.Web/wwwroot/_references.js b/Web/Resgrid.Web/wwwroot/_references.js index 2d2c7d476..d891e448a 100644 --- a/Web/Resgrid.Web/wwwroot/_references.js +++ b/Web/Resgrid.Web/wwwroot/_references.js @@ -665,6 +665,7 @@ /// /// /// +/// /// /// /// diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.byunit.js b/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.byunit.js new file mode 100644 index 000000000..0a93dfc69 --- /dev/null +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.byunit.js @@ -0,0 +1,36 @@ +var resgrid; +(function (resgrid) { + var inventory; + (function (inventory) { + var byunit; + (function (byunit) { + $(document).ready(function () { + resgrid.common.analytics.track('Inventory - By Unit'); + var strings = typeof inventoryByUnitStrings !== 'undefined' ? inventoryByUnitStrings : { + unit: 'Unit', type: 'Type', amount: 'Amount', group: 'Group', batch: 'Batch', + timestamp: 'Timestamp', addedBy: 'Added By', actions: 'Actions', view: 'View' + }; + $("#inventoryByUnitList").DataTable({ + ajax: { url: resgrid.absoluteBaseUrl + '/User/Inventory/GetInventoryByUnitList', dataSrc: '' }, + pageLength: 50, + order: [[0, 'asc']], + columns: [ + { data: 'Unit', title: strings.unit }, + { data: 'Type', title: strings.type }, + { data: 'Amount', title: strings.amount }, + { data: 'Group', title: strings.group }, + { data: 'Batch', title: strings.batch }, + { data: 'Timestamp', title: strings.timestamp }, + { data: 'UserName', title: strings.addedBy }, + { + data: 'InventoryId', title: strings.actions, orderable: false, + render: function (data) { + return '' + strings.view + ''; + } + } + ] + }); + }); + })(byunit = inventory.byunit || (inventory.byunit = {})); + })(inventory = resgrid.inventory || (resgrid.inventory = {})); +})(resgrid || (resgrid = {})); diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.history.js b/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.history.js index 5a65a29ee..a559bb2eb 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.history.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/inventory/resgrid.inventory.history.js @@ -7,7 +7,7 @@ var resgrid; $(document).ready(function () { resgrid.common.analytics.track('Inventory - History'); var strings = typeof inventoryHistoryStrings !== 'undefined' ? inventoryHistoryStrings : { - type: 'Type', amount: 'Amount', group: 'Group', batch: 'Batch', + type: 'Type', amount: 'Amount', group: 'Group', unit: 'Unit', batch: 'Batch', timestamp: 'Timestamp', addedBy: 'Added By', actions: 'Actions', view: 'View' }; $("#inventoryList").DataTable({ @@ -17,6 +17,7 @@ var resgrid; { data: 'Type', title: strings.type }, { data: 'Amount', title: strings.amount }, { data: 'Group', title: strings.group }, + { data: 'Unit', title: strings.unit }, { data: 'Batch', title: strings.batch }, { data: 'Timestamp', title: strings.timestamp }, { data: 'UserName', title: strings.addedBy }, diff --git a/Workers/Resgrid.Workers.Framework/Logic/AuditQueueLogic.cs b/Workers/Resgrid.Workers.Framework/Logic/AuditQueueLogic.cs index c3f82e181..417ac8db6 100644 --- a/Workers/Resgrid.Workers.Framework/Logic/AuditQueueLogic.cs +++ b/Workers/Resgrid.Workers.Framework/Logic/AuditQueueLogic.cs @@ -778,14 +778,24 @@ public class AuditQueueLogic } // end switch + // Fallback so no audit event is ever silently dropped. Any AuditLogTypes value that + // doesn't have an explicit case above (e.g. newly added types) would otherwise leave + // Message empty and never be persisted. Build a generic, human-readable message + // instead of discarding the event. + if (String.IsNullOrWhiteSpace(auditLog.Message)) + { + var auditService = Bootstrapper.GetKernel().Resolve(); + var actor = profile?.FullName?.AsFirstNameLastName; + var action = auditService.GetAuditLogTypeString(auditEvent.Type); + + auditLog.Message = String.IsNullOrWhiteSpace(actor) ? action : $"{actor} - {action}"; + } + if (String.IsNullOrWhiteSpace(auditLog.Data)) auditLog.Data = "No Data"; - if (!String.IsNullOrWhiteSpace(auditLog.Message)) - { - auditLog.LoggedOn = DateTime.UtcNow; - await auditLogsRepository.SaveOrUpdateAsync(auditLog, cancellationToken); - } + auditLog.LoggedOn = DateTime.UtcNow; + await auditLogsRepository.SaveOrUpdateAsync(auditLog, cancellationToken); } catch (Exception ex) { From f84424776b315c48923eeb8e22729e700d5d2ed6 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 3 Jun 2026 12:10:56 -0500 Subject: [PATCH 2/2] RE1-T120 Audit log repo changes --- .../Repositories/IAuditLogsRepository.cs | 17 +++- .../IPaymentProviderEventsRepository.cs | 11 ++- .../Repositories/ISystemAuditsRepository.cs | 27 ++++- Core/Resgrid.Model/Services/IAuditService.cs | 14 ++- Core/Resgrid.Services/AuditService.cs | 6 ++ .../AuditLogsRepository.cs | 63 +++++++++++- .../PaymentProviderEventsRepository.cs | 50 +++++++++- ...tAuditLogsForDepartmentByTypePagedQuery.cs | 23 +++++ .../SelectAuditLogsForDepartmentPagedQuery.cs | 23 +++++ ...tPaymentProviderEventsByCustomerIdQuery.cs | 23 +++++ ...ectSystemAuditsByDepartmentIdPagedQuery.cs | 23 +++++ .../SelectSystemAuditsByUserIdPagedQuery.cs | 23 +++++ .../SystemAuditRepository.cs | 99 ++++++++++++++++++- 13 files changed, 395 insertions(+), 7 deletions(-) create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentByTypePagedQuery.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentPagedQuery.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/Payments/SelectPaymentProviderEventsByCustomerIdQuery.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByDepartmentIdPagedQuery.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByUserIdPagedQuery.cs diff --git a/Core/Resgrid.Model/Repositories/IAuditLogsRepository.cs b/Core/Resgrid.Model/Repositories/IAuditLogsRepository.cs index 175d4424b..6f4ccd100 100644 --- a/Core/Resgrid.Model/Repositories/IAuditLogsRepository.cs +++ b/Core/Resgrid.Model/Repositories/IAuditLogsRepository.cs @@ -1,4 +1,8 @@ -namespace Resgrid.Model.Repositories +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories { /// /// Interface IAuditLogsRepository @@ -7,5 +11,16 @@ /// public interface IAuditLogsRepository: IRepository { + /// + /// Gets a date-ranged, optionally type-filtered, paged set of audit logs for a department. + /// + /// The department identifier. + /// Inclusive lower bound on LoggedOn (UTC). + /// Exclusive upper bound on LoggedOn (UTC). + /// Optional LogType filter; when null all types are returned. + /// 1-based page number. + /// Page size. + /// Task<IEnumerable<AuditLog>>. + Task> GetAuditLogsForDepartmentPagedAsync(int departmentId, DateTime startDate, DateTime endDate, int? logType, int page, int pageSize); } } diff --git a/Core/Resgrid.Model/Repositories/IPaymentProviderEventsRepository.cs b/Core/Resgrid.Model/Repositories/IPaymentProviderEventsRepository.cs index f68b044fe..4864ac702 100644 --- a/Core/Resgrid.Model/Repositories/IPaymentProviderEventsRepository.cs +++ b/Core/Resgrid.Model/Repositories/IPaymentProviderEventsRepository.cs @@ -1,4 +1,7 @@ -namespace Resgrid.Model.Repositories +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories { /// /// Interface IPaymentProviderEventsRepository @@ -7,5 +10,11 @@ /// public interface IPaymentProviderEventsRepository: IRepository { + /// + /// Gets all provider events for a payment-provider customer id (e.g. a Stripe customer), newest first. + /// + /// The payment-provider customer identifier. + /// Task<IEnumerable<PaymentProviderEvent>>. + Task> GetByCustomerIdAsync(string customerId); } } diff --git a/Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs b/Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs index a512e3ea6..127a49417 100644 --- a/Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs +++ b/Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs @@ -1,4 +1,8 @@ -namespace Resgrid.Model.Repositories +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories { /// /// Interface ISystemAuditsRepository @@ -7,5 +11,26 @@ /// public interface ISystemAuditsRepository : IRepository { + /// + /// Gets a date-ranged, paged set of system audits for a user (e.g. a login timeline). + /// + /// The user identifier. + /// Inclusive lower bound on LoggedOn (UTC). + /// Exclusive upper bound on LoggedOn (UTC). + /// 1-based page number. + /// Page size. + /// Task<IEnumerable<SystemAudit>>. + Task> GetByUserIdPagedAsync(string userId, DateTime startDate, DateTime endDate, int page, int pageSize); + + /// + /// Gets a date-ranged, paged set of system audits for a department. + /// + /// The department identifier. + /// Inclusive lower bound on LoggedOn (UTC). + /// Exclusive upper bound on LoggedOn (UTC). + /// 1-based page number. + /// Page size. + /// Task<IEnumerable<SystemAudit>>. + Task> GetByDepartmentIdPagedAsync(int departmentId, DateTime startDate, DateTime endDate, int page, int pageSize); } } diff --git a/Core/Resgrid.Model/Services/IAuditService.cs b/Core/Resgrid.Model/Services/IAuditService.cs index 382e3f2ef..ce78beca0 100644 --- a/Core/Resgrid.Model/Services/IAuditService.cs +++ b/Core/Resgrid.Model/Services/IAuditService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -10,5 +11,16 @@ public interface IAuditService Task> GetAllAuditLogsForDepartmentAsync(int departmentId); string GetAuditLogTypeString(AuditLogTypes logType); Task GetAuditLogByIdAsync(int auditLogId); + + /// + /// Gets a date-ranged, optionally type-filtered, paged set of audit logs for a department. + /// + /// The department identifier. + /// Inclusive lower bound on LoggedOn (UTC). + /// Exclusive upper bound on LoggedOn (UTC). + /// Optional LogType filter; when null all types are returned. + /// 1-based page number. + /// Page size. + Task> GetAuditLogsForDepartmentPagedAsync(int departmentId, DateTime startDate, DateTime endDate, AuditLogTypes? logType, int page, int pageSize); } } diff --git a/Core/Resgrid.Services/AuditService.cs b/Core/Resgrid.Services/AuditService.cs index 43d1bcb44..4a4c34e35 100644 --- a/Core/Resgrid.Services/AuditService.cs +++ b/Core/Resgrid.Services/AuditService.cs @@ -37,6 +37,12 @@ public async Task> GetAllAuditLogsForDepartmentAsync(int departme return logs.ToList(); } + public async Task> GetAuditLogsForDepartmentPagedAsync(int departmentId, DateTime startDate, DateTime endDate, AuditLogTypes? logType, int page, int pageSize) + { + var logs = await _auditLogsRepository.GetAuditLogsForDepartmentPagedAsync(departmentId, startDate, endDate, (int?)logType, page, pageSize); + return logs.ToList(); + } + public string GetAuditLogTypeString(AuditLogTypes logType) { switch (logType) diff --git a/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs index 7df0b335b..561f94762 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/AuditLogsRepository.cs @@ -1,8 +1,15 @@ -using Resgrid.Model; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Framework; +using Resgrid.Model; using Resgrid.Model.Repositories; using Resgrid.Model.Repositories.Connection; using Resgrid.Model.Repositories.Queries; using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Queries.AuditLogs; namespace Resgrid.Repositories.DataRepository { @@ -21,5 +28,59 @@ public AuditLogsRepository(IConnectionProvider connectionProvider, SqlConfigurat _queryFactory = queryFactory; _unitOfWork = unitOfWork; } + + public async Task> GetAuditLogsForDepartmentPagedAsync(int departmentId, DateTime startDate, DateTime endDate, int? logType, int page, int pageSize) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + dynamicParameters.Add("StartDate", startDate); + dynamicParameters.Add("EndDate", endDate); + dynamicParameters.Add("Offset", (page - 1) * pageSize); + dynamicParameters.Add("PageSize", pageSize); + + string query; + if (logType.HasValue) + { + dynamicParameters.Add("LogType", logType.Value); + query = _queryFactory.GetQuery(); + } + else + { + query = _queryFactory.GetQuery(); + } + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex, extraMessage: $"GetAuditLogsForDepartmentPagedAsync DepartmentId: {departmentId}"); + + throw; + } + } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/PaymentProviderEventsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/PaymentProviderEventsRepository.cs index 035a07cf2..ff6bac888 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/PaymentProviderEventsRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/PaymentProviderEventsRepository.cs @@ -1,8 +1,15 @@ -using Resgrid.Model; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Framework; +using Resgrid.Model; using Resgrid.Model.Repositories; using Resgrid.Model.Repositories.Connection; using Resgrid.Model.Repositories.Queries; using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Queries.Payments; namespace Resgrid.Repositories.DataRepository { @@ -21,5 +28,46 @@ public PaymentProviderEventsRepository(IConnectionProvider connectionProvider, S _queryFactory = queryFactory; _unitOfWork = unitOfWork; } + + public async Task> GetByCustomerIdAsync(string customerId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CustomerId", customerId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + + throw; + } + } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentByTypePagedQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentByTypePagedQuery.cs new file mode 100644 index 000000000..42c8eb5e3 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentByTypePagedQuery.cs @@ -0,0 +1,23 @@ +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository.Queries.AuditLogs +{ + public class SelectAuditLogsForDepartmentByTypePagedQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectAuditLogsForDepartmentByTypePagedQuery(SqlConfiguration sqlConfiguration) => _sqlConfiguration = sqlConfiguration; + + public string GetQuery() + { + if (DataConfig.DatabaseType == DatabaseTypes.Postgres) + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.auditlogs WHERE departmentid = {_sqlConfiguration.ParameterNotation}DepartmentId AND logtype = {_sqlConfiguration.ParameterNotation}LogType AND loggedon >= {_sqlConfiguration.ParameterNotation}StartDate AND loggedon < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY loggedon DESC LIMIT {_sqlConfiguration.ParameterNotation}PageSize OFFSET {_sqlConfiguration.ParameterNotation}Offset"; + + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.[AuditLogs] WHERE [DepartmentId] = {_sqlConfiguration.ParameterNotation}DepartmentId AND [LogType] = {_sqlConfiguration.ParameterNotation}LogType AND [LoggedOn] >= {_sqlConfiguration.ParameterNotation}StartDate AND [LoggedOn] < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY [LoggedOn] DESC OFFSET {_sqlConfiguration.ParameterNotation}Offset ROWS FETCH NEXT {_sqlConfiguration.ParameterNotation}PageSize ROWS ONLY"; + } + + public string GetQuery() where TEntity : class, IEntity => GetQuery(); + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentPagedQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentPagedQuery.cs new file mode 100644 index 000000000..fa74c0d86 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/AuditLogs/SelectAuditLogsForDepartmentPagedQuery.cs @@ -0,0 +1,23 @@ +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository.Queries.AuditLogs +{ + public class SelectAuditLogsForDepartmentPagedQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectAuditLogsForDepartmentPagedQuery(SqlConfiguration sqlConfiguration) => _sqlConfiguration = sqlConfiguration; + + public string GetQuery() + { + if (DataConfig.DatabaseType == DatabaseTypes.Postgres) + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.auditlogs WHERE departmentid = {_sqlConfiguration.ParameterNotation}DepartmentId AND loggedon >= {_sqlConfiguration.ParameterNotation}StartDate AND loggedon < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY loggedon DESC LIMIT {_sqlConfiguration.ParameterNotation}PageSize OFFSET {_sqlConfiguration.ParameterNotation}Offset"; + + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.[AuditLogs] WHERE [DepartmentId] = {_sqlConfiguration.ParameterNotation}DepartmentId AND [LoggedOn] >= {_sqlConfiguration.ParameterNotation}StartDate AND [LoggedOn] < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY [LoggedOn] DESC OFFSET {_sqlConfiguration.ParameterNotation}Offset ROWS FETCH NEXT {_sqlConfiguration.ParameterNotation}PageSize ROWS ONLY"; + } + + public string GetQuery() where TEntity : class, IEntity => GetQuery(); + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Payments/SelectPaymentProviderEventsByCustomerIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Payments/SelectPaymentProviderEventsByCustomerIdQuery.cs new file mode 100644 index 000000000..c7dae34a7 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Payments/SelectPaymentProviderEventsByCustomerIdQuery.cs @@ -0,0 +1,23 @@ +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository.Queries.Payments +{ + public class SelectPaymentProviderEventsByCustomerIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectPaymentProviderEventsByCustomerIdQuery(SqlConfiguration sqlConfiguration) => _sqlConfiguration = sqlConfiguration; + + public string GetQuery() + { + if (DataConfig.DatabaseType == DatabaseTypes.Postgres) + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.paymentproviderevents WHERE customerid = {_sqlConfiguration.ParameterNotation}CustomerId ORDER BY recievedon DESC"; + + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.[PaymentProviderEvents] WHERE [CustomerId] = {_sqlConfiguration.ParameterNotation}CustomerId ORDER BY [RecievedOn] DESC"; + } + + public string GetQuery() where TEntity : class, IEntity => GetQuery(); + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByDepartmentIdPagedQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByDepartmentIdPagedQuery.cs new file mode 100644 index 000000000..239124a19 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByDepartmentIdPagedQuery.cs @@ -0,0 +1,23 @@ +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository.Queries.SystemAudits +{ + public class SelectSystemAuditsByDepartmentIdPagedQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectSystemAuditsByDepartmentIdPagedQuery(SqlConfiguration sqlConfiguration) => _sqlConfiguration = sqlConfiguration; + + public string GetQuery() + { + if (DataConfig.DatabaseType == DatabaseTypes.Postgres) + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.systemaudits WHERE departmentid = {_sqlConfiguration.ParameterNotation}DepartmentId AND loggedon >= {_sqlConfiguration.ParameterNotation}StartDate AND loggedon < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY loggedon DESC LIMIT {_sqlConfiguration.ParameterNotation}PageSize OFFSET {_sqlConfiguration.ParameterNotation}Offset"; + + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.[SystemAudits] WHERE [DepartmentId] = {_sqlConfiguration.ParameterNotation}DepartmentId AND [LoggedOn] >= {_sqlConfiguration.ParameterNotation}StartDate AND [LoggedOn] < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY [LoggedOn] DESC OFFSET {_sqlConfiguration.ParameterNotation}Offset ROWS FETCH NEXT {_sqlConfiguration.ParameterNotation}PageSize ROWS ONLY"; + } + + public string GetQuery() where TEntity : class, IEntity => GetQuery(); + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByUserIdPagedQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByUserIdPagedQuery.cs new file mode 100644 index 000000000..79d27bd90 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByUserIdPagedQuery.cs @@ -0,0 +1,23 @@ +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository.Queries.SystemAudits +{ + public class SelectSystemAuditsByUserIdPagedQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectSystemAuditsByUserIdPagedQuery(SqlConfiguration sqlConfiguration) => _sqlConfiguration = sqlConfiguration; + + public string GetQuery() + { + if (DataConfig.DatabaseType == DatabaseTypes.Postgres) + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.systemaudits WHERE userid = {_sqlConfiguration.ParameterNotation}UserId AND loggedon >= {_sqlConfiguration.ParameterNotation}StartDate AND loggedon < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY loggedon DESC LIMIT {_sqlConfiguration.ParameterNotation}PageSize OFFSET {_sqlConfiguration.ParameterNotation}Offset"; + + return $"SELECT * FROM {_sqlConfiguration.SchemaName}.[SystemAudits] WHERE [UserId] = {_sqlConfiguration.ParameterNotation}UserId AND [LoggedOn] >= {_sqlConfiguration.ParameterNotation}StartDate AND [LoggedOn] < {_sqlConfiguration.ParameterNotation}EndDate ORDER BY [LoggedOn] DESC OFFSET {_sqlConfiguration.ParameterNotation}Offset ROWS FETCH NEXT {_sqlConfiguration.ParameterNotation}PageSize ROWS ONLY"; + } + + public string GetQuery() where TEntity : class, IEntity => GetQuery(); + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs index f23ac1984..4aca22109 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs @@ -1,8 +1,15 @@ -using Resgrid.Model; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Framework; +using Resgrid.Model; using Resgrid.Model.Repositories; using Resgrid.Model.Repositories.Connection; using Resgrid.Model.Repositories.Queries; using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Queries.SystemAudits; namespace Resgrid.Repositories.DataRepository { @@ -21,5 +28,95 @@ public SystemAuditsRepository(IConnectionProvider connectionProvider, SqlConfigu _queryFactory = queryFactory; _unitOfWork = unitOfWork; } + + public async Task> GetByUserIdPagedAsync(string userId, DateTime startDate, DateTime endDate, int page, int pageSize) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("UserId", userId); + dynamicParameters.Add("StartDate", startDate); + dynamicParameters.Add("EndDate", endDate); + dynamicParameters.Add("Offset", (page - 1) * pageSize); + dynamicParameters.Add("PageSize", pageSize); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex, extraMessage: $"GetByUserIdPagedAsync UserId: {userId}"); + + throw; + } + } + + public async Task> GetByDepartmentIdPagedAsync(int departmentId, DateTime startDate, DateTime endDate, int page, int pageSize) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + dynamicParameters.Add("StartDate", startDate); + dynamicParameters.Add("EndDate", endDate); + dynamicParameters.Add("Offset", (page - 1) * pageSize); + dynamicParameters.Add("PageSize", pageSize); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex, extraMessage: $"GetByDepartmentIdPagedAsync DepartmentId: {departmentId}"); + + throw; + } + } } }