diff --git a/src/Plugins/Shipping.ByWeight/Areas/Admin/Controllers/ShippingByWeightController.cs b/src/Plugins/Shipping.ByWeight/Areas/Admin/Controllers/ShippingByWeightController.cs index 758218bbe0..35d5c4cbfe 100644 --- a/src/Plugins/Shipping.ByWeight/Areas/Admin/Controllers/ShippingByWeightController.cs +++ b/src/Plugins/Shipping.ByWeight/Areas/Admin/Controllers/ShippingByWeightController.cs @@ -92,7 +92,8 @@ public async Task SaveGeneralSettings(ShippingByWeightListModel m [AutoValidateAntiforgeryToken] public async Task RatesList(DataSourceRequest command) { - var records = await _shippingByWeightService.GetAll(command.Page - 1, command.PageSize); + //main admin sees the records of every store + var records = await _shippingByWeightService.GetAll("", command.Page - 1, command.PageSize); var sbwModel = new List(); foreach (var x in records) diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Controllers/ShippingByWeightController.cs b/src/Plugins/Shipping.ByWeight/Areas/Store/Controllers/ShippingByWeightController.cs new file mode 100644 index 0000000000..c48f82d2dd --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Controllers/ShippingByWeightController.cs @@ -0,0 +1,351 @@ +using Grand.Business.Core.Interfaces.Catalog.Directory; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Configuration; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Directory; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Web.Common.Controllers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Shipping.ByWeight.Domain; +using Shipping.ByWeight.Models; +using Shipping.ByWeight.Services; +using System.Text; + +namespace Shipping.ByWeight.Areas.Store.Controllers; + +/// +/// Store-manager configuration of the "by weight" shipping rate provider. +/// A store owner can add, edit and delete rate records, but only the ones that belong +/// to his own store (). Records owned by other stores or +/// global (store = *) records are never returned nor mutated, and the plugin settings +/// are saved as a per-store override rather than system wide. +/// +[Area("Store")] +[AuthorizeStore] +[AuthorizeMenu] +[PermissionAuthorize(PermissionSystemName.ShippingSettings)] +public class ShippingByWeightController : BaseController +{ + private readonly IContextAccessor _contextAccessor; + private readonly ICountryService _countryService; + private readonly ICurrencyService _currencyService; + private readonly CurrencySettings _currencySettings; + private readonly IMeasureService _measureService; + private readonly MeasureSettings _measureSettings; + private readonly ISettingService _settingService; + private readonly IShippingByWeightService _shippingByWeightService; + private readonly IShippingMethodService _shippingMethodService; + private readonly ITranslationService _translationService; + private readonly IWarehouseService _warehouseService; + + public ShippingByWeightController( + IWarehouseService warehouseService, + IShippingMethodService shippingMethodService, + ICountryService countryService, + IShippingByWeightService shippingByWeightService, + ISettingService settingService, + ITranslationService translationService, + ICurrencyService currencyService, + CurrencySettings currencySettings, + IMeasureService measureService, + MeasureSettings measureSettings, + IContextAccessor contextAccessor) + { + _warehouseService = warehouseService; + _shippingMethodService = shippingMethodService; + _countryService = countryService; + _shippingByWeightService = shippingByWeightService; + _settingService = settingService; + _translationService = translationService; + _currencyService = currencyService; + _currencySettings = currencySettings; + _measureService = measureService; + _measureSettings = measureSettings; + _contextAccessor = contextAccessor; + } + + /// + /// The store the current staff/store-manager is bound to. + /// + private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + public async Task Configure() + { + //per-store override of the plugin settings - the global values are the fallback + var settings = await _settingService.LoadSetting(CurrentStoreId); + + var model = new ShippingByWeightListModel { + LimitMethodsToCreated = settings.LimitMethodsToCreated, + DisplayOrder = settings.DisplayOrder + }; + + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task SaveGeneralSettings(ShippingByWeightListModel model) + { + var settings = await _settingService.LoadSetting(CurrentStoreId); + settings.LimitMethodsToCreated = model.LimitMethodsToCreated; + settings.DisplayOrder = model.DisplayOrder; + + //save as a per-store override - never touch the system-wide value + await _settingService.SaveSetting(settings, CurrentStoreId); + + return Json(new { Result = true }); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.List)] + public async Task RatesList(DataSourceRequest command) + { + //only the current store records - filtered and paged in the service layer + var records = await _shippingByWeightService.GetAll(CurrentStoreId, command.Page - 1, command.PageSize); + + var sbwModel = new List(); + foreach (var x in records) + { + var m = new ShippingByWeightModel { + Id = x.Id, + StoreId = x.StoreId, + WarehouseId = x.WarehouseId, + ShippingMethodId = x.ShippingMethodId, + CountryId = x.CountryId, + From = x.From, + To = x.To, + AdditionalFixedCost = x.AdditionalFixedCost, + PercentageRateOfSubtotal = x.PercentageRateOfSubtotal, + RatePerWeightUnit = x.RatePerWeightUnit, + LowerWeightLimit = x.LowerWeightLimit + }; + //shipping method + var shippingMethod = await _shippingMethodService.GetShippingMethodById(x.ShippingMethodId); + m.ShippingMethodName = shippingMethod != null ? shippingMethod.Name : "Unavailable"; + //warehouse + var warehouse = await _warehouseService.GetWarehouseById(x.WarehouseId); + m.WarehouseName = warehouse != null ? warehouse.Name : "*"; + //country + var c = await _countryService.GetCountryById(x.CountryId); + m.CountryName = c != null ? c.Name : "*"; + //state + var s = c?.StateProvinces.FirstOrDefault(y => y.Id == x.StateProvinceId); + m.StateProvinceName = s != null ? s.Name : "*"; + //zip + m.Zip = !string.IsNullOrEmpty(x.Zip) ? x.Zip : "*"; + + var htmlSb = new StringBuilder("
"); + htmlSb.Append($"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.From")}: {m.From}"); + htmlSb.Append("
"); + htmlSb.Append($"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.To")}: {m.To}"); + htmlSb.Append("
"); + htmlSb.Append( + $"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.AdditionalFixedCost")}: {m.AdditionalFixedCost}"); + htmlSb.Append("
"); + htmlSb.Append( + $"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.RatePerWeightUnit")}: {m.RatePerWeightUnit}"); + htmlSb.Append("
"); + htmlSb.Append( + $"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.LowerWeightLimit")}: {m.LowerWeightLimit}"); + htmlSb.Append("
"); + htmlSb.Append( + $"{_translationService.GetResource("Plugins.Shipping.ByWeight.Fields.PercentageRateOfSubtotal")}: {m.PercentageRateOfSubtotal}"); + htmlSb.Append("
"); + m.DataHtml = htmlSb.ToString(); + + sbwModel.Add(m); + } + + var gridModel = new DataSourceResult { + Data = sbwModel, + Total = records.TotalCount + }; + + return Json(gridModel); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task RateDelete(string id) + { + var sbw = await _shippingByWeightService.GetById(id); + //guard: a store owner can only delete his own store records + if (sbw == null || sbw.StoreId != CurrentStoreId) + return new JsonResult(""); + + await _shippingByWeightService.DeleteShippingByWeightRecord(sbw); + + return new JsonResult(""); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task AddPopup() + { + var model = new ShippingByWeightModel { + //the owner cannot create records for another store + StoreId = CurrentStoreId, + //CurrencySettings is resolved per current store, so this already honours a store override + PrimaryStoreCurrencyCode = + (await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode, + BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name, + To = 1000000 + }; + + var shippingMethods = await _shippingMethodService.GetAllShippingMethods(storeId: CurrentStoreId); + if (shippingMethods.Count == 0) + return Content("No shipping methods can be loaded"); + + await PrepareSelectLists(model, shippingMethods, null, null, null); + + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task AddPopup(ShippingByWeightModel model) + { + var sbw = new ShippingByWeightRecord { + //force the current store - the owner cannot create records for another store + StoreId = CurrentStoreId, + WarehouseId = model.WarehouseId, + CountryId = model.CountryId, + StateProvinceId = model.StateProvinceId, + Zip = model.Zip == "*" ? null : model.Zip, + ShippingMethodId = model.ShippingMethodId, + From = model.From, + To = model.To, + AdditionalFixedCost = model.AdditionalFixedCost, + RatePerWeightUnit = model.RatePerWeightUnit, + PercentageRateOfSubtotal = model.PercentageRateOfSubtotal, + LowerWeightLimit = model.LowerWeightLimit + }; + await _shippingByWeightService.InsertShippingByWeightRecord(sbw); + + ViewBag.RefreshPage = true; + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task EditPopup(string id) + { + var sbw = await _shippingByWeightService.GetById(id); + //guard: a store owner can only open his own store records + if (sbw == null || sbw.StoreId != CurrentStoreId) + return RedirectToAction("Configure"); + + var model = new ShippingByWeightModel { + Id = sbw.Id, + StoreId = sbw.StoreId, + WarehouseId = sbw.WarehouseId, + CountryId = sbw.CountryId, + StateProvinceId = sbw.StateProvinceId, + Zip = sbw.Zip, + ShippingMethodId = sbw.ShippingMethodId, + From = sbw.From, + To = sbw.To, + AdditionalFixedCost = sbw.AdditionalFixedCost, + PercentageRateOfSubtotal = sbw.PercentageRateOfSubtotal, + RatePerWeightUnit = sbw.RatePerWeightUnit, + LowerWeightLimit = sbw.LowerWeightLimit, + //CurrencySettings is resolved per current store, so this already honours a store override + PrimaryStoreCurrencyCode = + (await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode, + BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name + }; + + var shippingMethods = await _shippingMethodService.GetAllShippingMethods(storeId: CurrentStoreId); + if (shippingMethods.Count == 0) + return Content("No shipping methods can be loaded"); + + await PrepareSelectLists(model, shippingMethods, sbw.WarehouseId, sbw.ShippingMethodId, sbw.CountryId, + sbw.StateProvinceId); + + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task EditPopup(ShippingByWeightModel model) + { + var sbw = await _shippingByWeightService.GetById(model.Id); + //guard: a store owner can only edit his own store records + if (sbw == null || sbw.StoreId != CurrentStoreId) + return RedirectToAction("Configure"); + + //StoreId is deliberately not taken from the model - the record stays in the owner's store + sbw.WarehouseId = model.WarehouseId; + sbw.CountryId = model.CountryId; + sbw.StateProvinceId = model.StateProvinceId; + sbw.Zip = model.Zip == "*" ? null : model.Zip; + sbw.ShippingMethodId = model.ShippingMethodId; + sbw.From = model.From; + sbw.To = model.To; + sbw.AdditionalFixedCost = model.AdditionalFixedCost; + sbw.RatePerWeightUnit = model.RatePerWeightUnit; + sbw.PercentageRateOfSubtotal = model.PercentageRateOfSubtotal; + sbw.LowerWeightLimit = model.LowerWeightLimit; + await _shippingByWeightService.UpdateShippingByWeightRecord(sbw); + + ViewBag.RefreshPage = true; + + return View(model); + } + + /// + /// Fills the drop-downs of the add/edit form. No store selector is offered - the record + /// always belongs to the owner's own store - and warehouses are limited to the ones the + /// store may use (its own, plus the ones shared by every store). + /// + [NonAction] + private async Task PrepareSelectLists(ShippingByWeightModel model, + IList shippingMethods, + string selectedWarehouseId, string selectedShippingMethodId, string selectedCountryId, + string selectedStateProvinceId = null) + { + //warehouses + model.AvailableWarehouses.Add(new SelectListItem { Text = "*", Value = "" }); + var warehouses = (await _warehouseService.GetAllWarehouses()) + .Where(x => string.IsNullOrEmpty(x.StoreId) || x.StoreId == CurrentStoreId); + foreach (var warehouse in warehouses) + model.AvailableWarehouses.Add(new SelectListItem { + Text = warehouse.Name, Value = warehouse.Id, + Selected = warehouse.Id == selectedWarehouseId + }); + //shipping methods + foreach (var sm in shippingMethods) + model.AvailableShippingMethods.Add(new SelectListItem { + Text = sm.Name, Value = sm.Id, + Selected = sm.Id == selectedShippingMethodId + }); + //countries + model.AvailableCountries.Add(new SelectListItem { Text = "*", Value = "" }); + var countries = await _countryService.GetAllCountries(showHidden: true); + foreach (var c in countries) + model.AvailableCountries.Add(new SelectListItem { + Text = c.Name, Value = c.Id, + Selected = c.Id == selectedCountryId + }); + //states + model.AvailableStates.Add(new SelectListItem { Text = "*", Value = "" }); + var selectedCountry = countries.FirstOrDefault(x => x.Id == selectedCountryId); + if (selectedCountry == null) return; + + foreach (var s in await _countryService.GetStateProvincesByCountryId(selectedCountry.Id)) + model.AvailableStates.Add(new SelectListItem { + Text = s.Name, Value = s.Id, + Selected = s.Id == selectedStateProvinceId + }); + } +} diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/AddPopup.cshtml b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/AddPopup.cshtml new file mode 100644 index 0000000000..cd5e9f4a60 --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/AddPopup.cshtml @@ -0,0 +1,41 @@ +@{ + Layout = ""; +} +@model Shipping.ByWeight.Models.ShippingByWeightModel +
+ +
+
+
+
+
+ + @Loc["Admin.Common.AddNew"] +
+
+
+ +
+
+
+
+ +
diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/Configure.cshtml b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/Configure.cshtml new file mode 100644 index 0000000000..360e3fd9af --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/Configure.cshtml @@ -0,0 +1,235 @@ +@model Shipping.ByWeight.Models.ShippingByWeightListModel +@{ + Layout = "~/Areas/Store/Views/Shared/_StoreLayout.cshtml"; + ViewBag.Title = Loc["Admin.Configuration.Shipping.Providers"]; +} + + +
+ +
+ @Loc["Plugins.Shipping.ByWeight.Formula"]: + @Loc["Plugins.Shipping.ByWeight.Formula.Value"] +
+ + @{ + var addNewUrl = Url.Action("AddPopup", "ShippingByWeight", new { area = "Store", btnId = "btnRefresh", formId = "shipping-byweight-form" }); + } + +
+
+
+
+
+ + @Loc["Shipping.ByWeight.FriendlyName"] +
+
+
+
+
+ +
+
+
+ + + + + +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/EditPopup.cshtml b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/EditPopup.cshtml new file mode 100644 index 0000000000..3d2caf8deb --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/EditPopup.cshtml @@ -0,0 +1,41 @@ +@{ + Layout = ""; +} +@model Shipping.ByWeight.Models.ShippingByWeightModel +
+ +
+
+
+
+
+ + @Loc["Admin.Common.Edit"] +
+
+
+ +
+
+
+
+ +
diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/_CreateOrUpdate.cshtml b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/_CreateOrUpdate.cshtml new file mode 100644 index 0000000000..afd540a081 --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/ShippingByWeight/_CreateOrUpdate.cshtml @@ -0,0 +1,148 @@ +@using Microsoft.AspNetCore.Routing +@model Shipping.ByWeight.Models.ShippingByWeightModel +
+ + +@if (ViewBag.RefreshPage == true) +{ + +} + + +@*the record always belongs to the owner's own store, so no store selector is offered*@ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ [@Model.BaseWeightIn] + +
+
+
+
+ +
+
+ [@Model.BaseWeightIn] + +
+
+
+
+ +
+
+ [@Model.PrimaryStoreCurrencyCode] + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ [@Model.PrimaryStoreCurrencyCode] + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
diff --git a/src/Plugins/Shipping.ByWeight/Areas/Store/Views/_ViewImports.cshtml b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..254d01f31e --- /dev/null +++ b/src/Plugins/Shipping.ByWeight/Areas/Store/Views/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Grand.Web.Common + +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@using Grand.Infrastructure.Extensions +@using Grand.Web.Common.Extensions +@using Grand.Web.Common.Localization + +@inject LocService Loc diff --git a/src/Plugins/Shipping.ByWeight/Services/IShippingByWeightService.cs b/src/Plugins/Shipping.ByWeight/Services/IShippingByWeightService.cs index 38b269f5b4..28048e90a4 100644 --- a/src/Plugins/Shipping.ByWeight/Services/IShippingByWeightService.cs +++ b/src/Plugins/Shipping.ByWeight/Services/IShippingByWeightService.cs @@ -7,7 +7,14 @@ public interface IShippingByWeightService { Task DeleteShippingByWeightRecord(ShippingByWeightRecord shippingByWeightRecord); - Task> GetAll(int pageIndex = 0, int pageSize = int.MaxValue); + /// + /// Gets shipping by weight records + /// + /// The store identifier; pass "" to load records of all stores + /// Page index + /// Page size + Task> GetAll(string storeId = "", int pageIndex = 0, + int pageSize = int.MaxValue); Task FindRecord(string shippingMethodId, string storeId, string warehouseId, diff --git a/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs b/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs index 737616e991..88a7c2138d 100644 --- a/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs @@ -20,7 +20,7 @@ public ShippingByWeightService(ICacheBase cacheBase, #region Constants - private const string SHIPPINGBYWEIGHT_ALL_KEY = "Grand.shippingbyweight.all-{0}-{1}"; + private const string SHIPPINGBYWEIGHT_ALL_KEY = "Grand.shippingbyweight.all-{0}-{1}-{2}"; private const string SHIPPINGBYWEIGHT_PATTERN_KEY = "Grand.shippingbyweight."; #endregion @@ -43,13 +43,16 @@ public virtual async Task DeleteShippingByWeightRecord(ShippingByWeightRecord sh await _cacheBase.RemoveByPrefix(SHIPPINGBYWEIGHT_PATTERN_KEY); } - public virtual async Task> GetAll(int pageIndex = 0, int pageSize = int.MaxValue) + public virtual async Task> GetAll(string storeId = "", int pageIndex = 0, + int pageSize = int.MaxValue) { - var key = string.Format(SHIPPINGBYWEIGHT_ALL_KEY, pageIndex, pageSize); + var key = string.Format(SHIPPINGBYWEIGHT_ALL_KEY, storeId, pageIndex, pageSize); return await _cacheBase.GetAsync(key, async () => { - var query = from sbw in _sbwRepository.Table - select sbw; + var query = _sbwRepository.Table.AsQueryable(); + //filter by store when requested; an empty storeId returns records of all stores + if (!string.IsNullOrEmpty(storeId)) + query = query.Where(sbw => sbw.StoreId == storeId); return await _sbwRepository.PagedAsync(query, pageIndex, pageSize); }); diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Controllers/ShippingPointController.cs b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Controllers/ShippingPointController.cs new file mode 100644 index 0000000000..fe39d11154 --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Controllers/ShippingPointController.cs @@ -0,0 +1,178 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Web.Common.Controllers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Shipping.ShippingPoint.Models; +using Shipping.ShippingPoint.Services; + +namespace Shipping.ShippingPoint.Areas.Store.Controllers; + +/// +/// Store-manager configuration of the shipping point (pickup point) provider. +/// A store owner can add, edit and delete pickup points, but only the ones that belong +/// to his own store (). Points owned by other stores or +/// global (store = *) points are never returned nor mutated. +/// +[Area("Store")] +[AuthorizeStore] +[AuthorizeMenu] +[PermissionAuthorize(PermissionSystemName.ShippingSettings)] +public class ShippingPointController : BaseController +{ + private readonly IContextAccessor _contextAccessor; + private readonly ICountryService _countryService; + private readonly IShippingPointService _shippingPointService; + private readonly ITranslationService _translationService; + + public ShippingPointController( + ITranslationService translationService, + IShippingPointService shippingPointService, + ICountryService countryService, + IContextAccessor contextAccessor + ) + { + _translationService = translationService; + _shippingPointService = shippingPointService; + _countryService = countryService; + _contextAccessor = contextAccessor; + } + + /// + /// The store the current staff/store-manager is bound to. + /// + private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + public IActionResult Configure() + { + return View(); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.List)] + public async Task List(DataSourceRequest command) + { + //only the current store points - filtered and paged in the service layer + var shippingPoints = await _shippingPointService.GetAllStoreShippingPoint(CurrentStoreId, + command.Page - 1, command.PageSize); + + var viewModel = shippingPoints.Select(shippingPoint => new ShippingPointModel { + ShippingPointName = shippingPoint.ShippingPointName, + Description = shippingPoint.Description, + Id = shippingPoint.Id, + OpeningHours = shippingPoint.OpeningHours, + PickupFee = shippingPoint.PickupFee + }).ToList(); + + return Json(new DataSourceResult { + Data = viewModel, + Total = shippingPoints.TotalCount + }); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create() + { + var model = new ShippingPointModel { + //the owner cannot create points for another store + StoreId = CurrentStoreId + }; + await PrepareShippingPointModel(model); + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create(ShippingPointModel model) + { + if (ModelState.IsValid) + { + var shippingPoint = model.ToEntity(); + //force the current store - the owner cannot create points for another store + shippingPoint.StoreId = CurrentStoreId; + await _shippingPointService.InsertStoreShippingPoint(shippingPoint); + + ViewBag.RefreshPage = true; + return Content(""); + } + + await PrepareShippingPointModel(model); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var shippingPoint = await _shippingPointService.GetStoreShippingPointById(id); + //guard: a store owner can only open his own store points + if (shippingPoint == null || shippingPoint.StoreId != CurrentStoreId) + return RedirectToAction("Configure"); + + var model = shippingPoint.ToModel(); + await PrepareShippingPointModel(model); + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task Edit(ShippingPointModel model) + { + var existing = await _shippingPointService.GetStoreShippingPointById(model.Id); + //guard: a store owner can only edit his own store points + if (existing == null || existing.StoreId != CurrentStoreId) + return RedirectToAction("Configure"); + + if (ModelState.IsValid) + { + var shippingPoint = model.ToEntity(); + //StoreId is deliberately not taken from the model - the point stays in the owner's store + shippingPoint.StoreId = existing.StoreId; + await _shippingPointService.UpdateStoreShippingPoint(shippingPoint); + + return Content(""); + } + + ViewBag.RefreshPage = true; + + await PrepareShippingPointModel(model); + + return View(model); + } + + [HttpPost] + [AutoValidateAntiforgeryToken] + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task Delete(string id) + { + var shippingPoint = await _shippingPointService.GetStoreShippingPointById(id); + //guard: a store owner can only delete his own store points + if (shippingPoint == null || shippingPoint.StoreId != CurrentStoreId) + return new JsonResult(""); + + await _shippingPointService.DeleteStoreShippingPoint(shippingPoint); + + return new JsonResult(""); + } + + /// + /// Fills the drop-downs of the add/edit form. No store selector is offered - the point + /// always belongs to the owner's own store. + /// + [NonAction] + private async Task PrepareShippingPointModel(ShippingPointModel model) + { + model.AvailableCountries.Add(new SelectListItem + { Text = _translationService.GetResource("Admin.Address.SelectCountry"), Value = string.Empty }); + foreach (var country in await _countryService.GetAllCountries(showHidden: true)) + model.AvailableCountries.Add(new SelectListItem { Text = country.Name, Value = country.Id }); + } +} diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Configure.cshtml b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Configure.cshtml new file mode 100644 index 0000000000..ad6d55b091 --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Configure.cshtml @@ -0,0 +1,144 @@ +@{ + Layout = "~/Areas/Store/Views/Shared/_StoreLayout.cshtml"; + ViewBag.Title = Loc["Admin.Configuration.Shipping.Providers"]; +} + + +
+
+
+
+
+
+ + @Loc["Shipping.ShippingPoint.FriendlyName"] +
+
+
+
+
+ +
+
+
+ + + + +
diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Create.cshtml b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Create.cshtml new file mode 100644 index 0000000000..e9a346befc --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Create.cshtml @@ -0,0 +1,64 @@ +@{ + Layout = ""; +} +@model Shipping.ShippingPoint.Models.ShippingPointModel + +
+ +
+
+
+
+
+ @Loc["Admin.Common.AddNew"] +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Edit.cshtml b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Edit.cshtml new file mode 100644 index 0000000000..1f3d9e4719 --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/Edit.cshtml @@ -0,0 +1,64 @@ +@{ + Layout = ""; +} +@model Shipping.ShippingPoint.Models.ShippingPointModel + +
+ +
+
+
+
+
+ @Loc["Admin.Common.Edit"] +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/_CreateOrUpdate.cshtml b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/_CreateOrUpdate.cshtml new file mode 100644 index 0000000000..bcbf042fea --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/ShippingPoint/_CreateOrUpdate.cshtml @@ -0,0 +1,75 @@ +@model Shipping.ShippingPoint.Models.ShippingPointModel + +
+ + +@if (ViewBag.RefreshPage == true) +{ + +} + +@*the point always belongs to the owner's own store, so no store selector is offered*@ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
diff --git a/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/_ViewImports.cshtml b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..254d01f31e --- /dev/null +++ b/src/Plugins/Shipping.ShippingPoint/Areas/Store/Views/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Grand.Web.Common + +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@using Grand.Infrastructure.Extensions +@using Grand.Web.Common.Extensions +@using Grand.Web.Common.Localization + +@inject LocService Loc diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Shipping/Providers.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Shipping/Providers.cshtml index bb28545a20..535b94297e 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Shipping/Providers.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Shipping/Providers.cshtml @@ -85,12 +85,12 @@ $(document).ready(function () { field: "FriendlyName", title: "@Loc["Admin.Configuration.Shipping.Providers.Fields.FriendlyName"]", width: 250, - template: '#=FriendlyName#', + template: '# if (ConfigurationUrl) { # #:FriendlyName# # } else { # #:FriendlyName# # } #', }, { field: "SystemName", title: "@Loc["Admin.Configuration.Shipping.Providers.Fields.SystemName"]", width: 250, - template: '#=SystemName#', + template: '# if (ConfigurationUrl) { # #:SystemName# # } else { # #:SystemName# # } #', }, { field: "DisplayOrder", title: "@Loc["Admin.Configuration.Shipping.Providers.Fields.DisplayOrder"]", diff --git a/src/Web/Grand.Web.Store/Controllers/ShippingController.cs b/src/Web/Grand.Web.Store/Controllers/ShippingController.cs index 017c2e5a6a..65491b5dc1 100644 --- a/src/Web/Grand.Web.Store/Controllers/ShippingController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ShippingController.cs @@ -17,6 +17,7 @@ using Grand.Web.Common.DataSource; using Grand.Web.Common.Models; using Grand.Web.Common.Security.Authorization; +using Grand.Web.Store.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; @@ -50,6 +51,10 @@ public async Task Providers(DataSourceRequest command) .Select(p => { var m = p.ToModel(); m.IsActive = p.IsShippingRateMethodActive(shippingProviderSettings); + //a provider whose plugin is not multi-store has no Store-area configuration screen - + //its relative url would resolve under /Store/ and 404, so offer no link at all + if (!StoreAreaConfiguration.Exists(p)) + m.ConfigurationUrl = null; return m; }) .ToList(); diff --git a/src/Web/Grand.Web.Store/Extensions/StoreAreaConfiguration.cs b/src/Web/Grand.Web.Store/Extensions/StoreAreaConfiguration.cs new file mode 100644 index 0000000000..01e564e938 --- /dev/null +++ b/src/Web/Grand.Web.Store/Extensions/StoreAreaConfiguration.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Mvc; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Grand.Web.Store.Extensions; + +/// +/// Tells whether the plugin a provider comes from ships a configuration screen for the Store area. +/// +/// Provider configuration urls are relative (e.g. "../ShippingByWeight/Configure"), so the very +/// same value resolves to /Admin/... under the Admin area and to /Store/... under this one. A +/// plugin whose data is not store-scoped only has an [Area("Admin")] controller - linking a store +/// owner there produces a 404. The Store grids therefore ask this first and render plain text +/// instead of a dead link when the answer is no. +/// +/// +public static class StoreAreaConfiguration +{ + //plugin assemblies never change within a process lifetime, so the reflection cost is paid once + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Whether the provider's plugin exposes at least one controller in the Store area. + /// + public static bool Exists(object provider) + { + return provider != null && Cache.GetOrAdd(provider.GetType().Assembly, HasStoreAreaController); + } + + private static bool HasStoreAreaController(Assembly assembly) + { + return GetLoadableTypes(assembly).Any(type => + typeof(ControllerBase).IsAssignableFrom(type) && + string.Equals(type.GetCustomAttribute()?.RouteValue, Constants.AreaStore, + StringComparison.OrdinalIgnoreCase)); + } + + /// + /// A plugin assembly can reference types that fail to load; those types are simply not controllers + /// we could route to, so they are skipped rather than failing the whole provider list. + /// + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(type => type != null); + } + } +}