-
Notifications
You must be signed in to change notification settings - Fork 550
Add CurrencyController to Grand.Web.Store for SaaS per-store currency management #698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
KrzysztofPajak
merged 18 commits into
develop
from
copilot/add-currency-controller-store
May 24, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
6c1f709
Add CurrencyController to Grand.Web.Store for SaaS store owner curren…
Copilot ea6e230
Fix review feedback: localize error messages, fix duplicate loc key i…
Copilot 61e3852
Hide unpublished currencies from store owner currency list
Copilot 81277a7
Fix antiforgery token rendering and add missing resource strings
Copilot 444b4c2
Convert AssignStore/UnassignStore to AJAX with grid refresh
Copilot 2c90d7f
Mark assigned currencies with green badge in Name column
Copilot 484e172
Add SetDefaultCurrency action and button for store owner default curr…
Copilot b9c7b4c
Validate currency is assigned to store before setting as default
Copilot 291b96d
Add dedicated IsDefaultStoreCurrency column with Set as Default button
Copilot a8ed5da
Add UnsetDefaultCurrency action and button in IsDefaultStoreCurrency …
Copilot 4888e49
Add GetAllPublishedCurrencies to ICurrencyService and use in Store Cu…
Copilot 16e9a55
Revert "Add GetAllPublishedCurrencies to ICurrencyService and use in …
KrzysztofPajak a9a9647
Potential fix for pull request finding
KrzysztofPajak 6251ed3
Allow global (non-limited) currencies to be set as store default
Copilot 87a13ac
Disable serverPaging/serverSorting/serverFiltering; remove unused Dat…
Copilot e09954f
Block UnassignStore when currency is store default
Copilot 46908c4
Add published guard in SetDefaultCurrency to reject unpublished curre…
Copilot 5628171
Update css
KrzysztofPajak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
216 changes: 216 additions & 0 deletions
216
src/Web/Grand.Web.Store/Areas/Store/Views/Currency/List.cshtml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| @inject AdminAreaSettings adminAreaSettings | ||
| @{ | ||
| ViewBag.Title = Loc["Admin.Configuration.Currencies"]; | ||
| Layout = Constants.LayoutStore; | ||
| } | ||
|
|
||
| <div class="row"> | ||
| <div class="col-md-12"> | ||
| <div class="x_panel light form-fit"> | ||
| <div class="x_title"> | ||
| <div class="caption"> | ||
| <i class="fa fa-money"></i> | ||
| @Loc["Admin.Configuration.Currencies"] | ||
| </div> | ||
| </div> | ||
| <div class="x_content form"> | ||
| <div id="currencies-grid"></div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <script> | ||
| $(document).ready(function () { | ||
| $("#currencies-grid").kendoGrid({ | ||
| dataSource: { | ||
| transport: { | ||
| read: { | ||
| url: "@Html.Raw(Url.Action("ListData", "Currency", new { area = Constants.AreaStore }))", | ||
| type: "POST", | ||
| dataType: "json", | ||
| data: addAntiForgeryToken | ||
| } | ||
| }, | ||
| schema: { | ||
| data: "Data", | ||
| total: "Total", | ||
| errors: "Errors" | ||
| }, | ||
| error: function (e) { | ||
| display_kendoui_grid_error(e); | ||
| this.cancelChanges(); | ||
| }, | ||
| pageSize: @(adminAreaSettings.DefaultGridPageSize), | ||
| serverPaging: false, | ||
| serverFiltering: false, | ||
| serverSorting: false | ||
| }, | ||
| pageable: { | ||
| refresh: true, | ||
| pageSizes: [@(adminAreaSettings.GridPageSizes)] | ||
| }, | ||
| scrollable: false, | ||
| columns: [ | ||
| { | ||
| field: "Name", | ||
| title: "@Loc["Admin.Configuration.Currencies.Fields.Name"]", | ||
| width: 200, | ||
| template: function (dataItem) { | ||
| var name = kendo.htmlEncode(dataItem.Name); | ||
| if (dataItem.IsPrimaryStoreCurrency) { | ||
| name += ' <span class="badge badge-warning" title="@Loc["Admin.Configuration.Currencies.Fields.IsPrimaryStoreCurrency"]"><i class="fa fa-star"></i></span>'; | ||
| } | ||
| if (dataItem.IsAssignedToCurrentStore) { | ||
| name += ' <span class="badge badge-success" title="@Loc["Admin.Configuration.Currencies.AssignStore"]"><i class="fa fa-check"></i></span>'; | ||
| } | ||
| return name; | ||
| } | ||
| }, | ||
| { | ||
| field: "CurrencyCode", | ||
| title: "@Loc["Admin.Configuration.Currencies.Fields.CurrencyCode"]", | ||
| width: 100 | ||
| }, | ||
| { | ||
| field: "Published", | ||
| title: "@Loc["Admin.Configuration.Currencies.Fields.Published"]", | ||
| width: 80, | ||
| headerAttributes: { style: "text-align:center" }, | ||
| attributes: { style: "text-align:center" }, | ||
| template: '# if(Published) {# <i class="fa fa-check" aria-hidden="true" style="color:green"></i> #} else {# <i class="fa fa-times" aria-hidden="true" style="color:red"></i> #} #' | ||
| }, | ||
| { | ||
| field: "LimitedToStores", | ||
| title: "@Loc["Admin.Configuration.Currencies.Fields.LimitedToStores"]", | ||
| width: 120, | ||
| headerAttributes: { style: "text-align:center" }, | ||
| attributes: { style: "text-align:center" }, | ||
| template: '# if(LimitedToStores) {# <i class="fa fa-check" aria-hidden="true" style="color:green"></i> #} else {# <i class="fa fa-times" aria-hidden="true" style="color:red"></i> #} #' | ||
| }, | ||
| { | ||
| field: "IsDefaultStoreCurrency", | ||
| title: "@Loc["Admin.Configuration.Currencies.SetDefaultCurrency"]", | ||
| width: 180, | ||
| headerAttributes: { style: "text-align:center" }, | ||
| attributes: { style: "text-align:center" }, | ||
| template: function (dataItem) { | ||
| if (dataItem.IsDefaultStoreCurrency) { | ||
| return '<span class="badge badge-info"><i class="fa fa-home"></i> @Loc["Admin.Configuration.Currencies.SetDefaultCurrency"]</span> ' + | ||
| '<a onclick="unsetDefaultCurrency(\'' + dataItem.Id + '\')" class="btn btn-sm btn-warning" title="@Loc["Admin.Configuration.Currencies.UnsetDefaultCurrency"]"><i class="fa fa-home"></i> @Loc["Admin.Configuration.Currencies.UnsetDefaultCurrency"]</a>'; | ||
| } | ||
| if (dataItem.IsAssignedToCurrentStore) { | ||
| return '<a onclick="setDefaultCurrency(\'' + dataItem.Id + '\')" class="btn btn-sm btn-info" title="@Loc["Admin.Configuration.Currencies.SetDefaultCurrency"]"><i class="fa fa-home"></i> @Loc["Admin.Configuration.Currencies.SetDefaultCurrency"]</a>'; | ||
| } | ||
| return '<span class="text-muted">-</span>'; | ||
| } | ||
| }, | ||
| { | ||
| title: "@Loc["Admin.Common.Actions"]", | ||
| width: 220, | ||
| headerAttributes: { style: "text-align:center" }, | ||
| attributes: { style: "text-align:center" }, | ||
| template: function (dataItem) { | ||
| if (!dataItem.CanManage) { | ||
| return '<span class="text-muted">-</span>'; | ||
| } | ||
| if (dataItem.IsAssignedToCurrentStore) { | ||
| return '<a onclick="unassignStore(\'' + dataItem.Id + '\')" class="btn btn-sm btn-danger" title="@Loc["Admin.Configuration.Currencies.UnassignStore"]"><i class="fa fa-minus"></i> @Loc["Admin.Configuration.Currencies.UnassignStore"]</a>'; | ||
| } | ||
| return '<a onclick="assignStore(\'' + dataItem.Id + '\')" class="btn btn-sm btn-success" title="@Loc["Admin.Configuration.Currencies.AssignStore"]"><i class="fa fa-plus"></i> @Loc["Admin.Configuration.Currencies.AssignStore"]</a>'; | ||
| } | ||
| } | ||
| ] | ||
| }); | ||
| }); | ||
|
|
||
| function assignStore(id) { | ||
| var postData = { id: id }; | ||
| addAntiForgeryToken(postData); | ||
| $.ajax({ | ||
| cache: false, | ||
| type: "POST", | ||
| url: "@Html.Raw(Url.Action("AssignStore", "Currency", new { area = Constants.AreaStore }))", | ||
| data: postData, | ||
| success: function (data) { | ||
| if (data.success) { | ||
| var grid = $("#currencies-grid").data('kendoGrid'); | ||
| grid.dataSource.read(); | ||
| } else { | ||
| alert(data.message); | ||
| } | ||
| }, | ||
| error: function () { | ||
| alert('Failed to update currency'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function unassignStore(id) { | ||
| if (!confirm('@Loc["Admin.Common.AreYouSure"]')) return; | ||
| var postData = { id: id }; | ||
| addAntiForgeryToken(postData); | ||
| $.ajax({ | ||
| cache: false, | ||
| type: "POST", | ||
| url: "@Html.Raw(Url.Action("UnassignStore", "Currency", new { area = Constants.AreaStore }))", | ||
| data: postData, | ||
| success: function (data) { | ||
| if (data.success) { | ||
| var grid = $("#currencies-grid").data('kendoGrid'); | ||
| grid.dataSource.read(); | ||
| } else { | ||
| alert(data.message); | ||
| } | ||
| }, | ||
| error: function () { | ||
| alert('Failed to update currency'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function setDefaultCurrency(id) { | ||
| var postData = { id: id }; | ||
| addAntiForgeryToken(postData); | ||
| $.ajax({ | ||
| cache: false, | ||
| type: "POST", | ||
| url: "@Html.Raw(Url.Action("SetDefaultCurrency", "Currency", new { area = Constants.AreaStore }))", | ||
| data: postData, | ||
| success: function (data) { | ||
| if (data.success) { | ||
| var grid = $("#currencies-grid").data('kendoGrid'); | ||
| grid.dataSource.read(); | ||
| } else { | ||
| alert(data.message); | ||
| } | ||
| }, | ||
| error: function () { | ||
| alert('Failed to set default currency'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function unsetDefaultCurrency(id) { | ||
| if (!confirm('@Loc["Admin.Common.AreYouSure"]')) return; | ||
| var postData = { id: id }; | ||
| addAntiForgeryToken(postData); | ||
| $.ajax({ | ||
| cache: false, | ||
| type: "POST", | ||
| url: "@Html.Raw(Url.Action("UnsetDefaultCurrency", "Currency", new { area = Constants.AreaStore }))", | ||
| data: postData, | ||
| success: function (data) { | ||
| if (data.success) { | ||
| var grid = $("#currencies-grid").data('kendoGrid'); | ||
| grid.dataSource.read(); | ||
| } else { | ||
| alert(data.message); | ||
| } | ||
| }, | ||
| error: function () { | ||
| alert('Failed to unset default currency'); | ||
| } | ||
| }); | ||
| } | ||
| </script> |
164 changes: 164 additions & 0 deletions
164
src/Web/Grand.Web.Store/Controllers/CurrencyController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| using Grand.Business.Core.Interfaces.Common.Directory; | ||
| using Grand.Business.Core.Interfaces.Common.Localization; | ||
| using Grand.Business.Core.Interfaces.Common.Stores; | ||
| using Grand.Domain.Directory; | ||
| using Grand.Domain.Permissions; | ||
| using Grand.Infrastructure; | ||
| using Grand.Web.Common.DataSource; | ||
| using Grand.Web.Common.Security.Authorization; | ||
| using Grand.Web.Store.Models; | ||
| using Microsoft.AspNetCore.Mvc; | ||
|
|
||
| namespace Grand.Web.Store.Controllers; | ||
|
|
||
| [PermissionAuthorize(PermissionSystemName.Currencies)] | ||
| public class CurrencyController( | ||
| ICurrencyService currencyService, | ||
| CurrencySettings currencySettings, | ||
| ITranslationService translationService, | ||
| IStoreService storeService, | ||
| IContextAccessor contextAccessor) : BaseStoreController | ||
| { | ||
| private string CurrentStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; | ||
|
|
||
| public IActionResult Index() | ||
| { | ||
| return RedirectToAction("List"); | ||
| } | ||
|
|
||
| [PermissionAuthorizeAction(PermissionActionName.List)] | ||
| public IActionResult List() | ||
| { | ||
| return View(); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [PermissionAuthorizeAction(PermissionActionName.List)] | ||
| public async Task<IActionResult> ListData() | ||
| { | ||
| var storeId = CurrentStoreId; | ||
| var primaryStoreCurrencyId = currencySettings.PrimaryStoreCurrencyId; | ||
|
|
||
| var store = await storeService.GetStoreById(storeId); | ||
| var defaultCurrencyId = store?.DefaultCurrencyId; | ||
|
|
||
| var currencies = await currencyService.GetAllCurrencies(showHidden: false); | ||
|
|
||
| var items = currencies | ||
| .Select(c => new StoreCurrencyModel { | ||
| Id = c.Id, | ||
| Name = c.Name, | ||
| CurrencyCode = c.CurrencyCode, | ||
| Published = c.Published, | ||
| DisplayOrder = c.DisplayOrder, | ||
| LimitedToStores = c.LimitedToStores, | ||
| IsAssignedToCurrentStore = !c.LimitedToStores || c.Stores.Contains(storeId), | ||
| IsPrimaryStoreCurrency = c.Id == primaryStoreCurrencyId, | ||
| IsDefaultStoreCurrency = c.Id == defaultCurrencyId, | ||
| CanManage = c.LimitedToStores | ||
| }) | ||
|
KrzysztofPajak marked this conversation as resolved.
|
||
| .ToList(); | ||
|
|
||
| var gridModel = new DataSourceResult { | ||
| Data = items, | ||
| Total = items.Count | ||
| }; | ||
|
|
||
| return Json(gridModel); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [PermissionAuthorizeAction(PermissionActionName.Edit)] | ||
| public async Task<IActionResult> AssignStore(string id) | ||
| { | ||
| var currency = await currencyService.GetCurrencyById(id); | ||
| if (currency == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotFound") }); | ||
|
|
||
| if (!currency.LimitedToStores) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.CannotModifyGlobal") }); | ||
|
|
||
| var storeId = CurrentStoreId; | ||
| if (!currency.Stores.Contains(storeId)) | ||
| { | ||
| currency.Stores.Add(storeId); | ||
| await currencyService.UpdateCurrency(currency); | ||
| } | ||
|
|
||
| return Json(new { success = true }); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [PermissionAuthorizeAction(PermissionActionName.Edit)] | ||
| public async Task<IActionResult> UnassignStore(string id) | ||
| { | ||
| var currency = await currencyService.GetCurrencyById(id); | ||
| if (currency == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotFound") }); | ||
|
|
||
| if (!currency.LimitedToStores) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.CannotModifyGlobal") }); | ||
|
|
||
| if (currency.Id == currencySettings.PrimaryStoreCurrencyId) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.CantDeletePrimary") }); | ||
|
|
||
| var storeId = CurrentStoreId; | ||
|
|
||
| var store = await storeService.GetStoreById(storeId); | ||
| if (store?.DefaultCurrencyId == currency.Id) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.CantUnassignDefault") }); | ||
|
|
||
| if (currency.Stores.Remove(storeId)) | ||
| await currencyService.UpdateCurrency(currency); | ||
|
|
||
| return Json(new { success = true }); | ||
|
KrzysztofPajak marked this conversation as resolved.
|
||
| } | ||
|
|
||
| [HttpPost] | ||
| [PermissionAuthorizeAction(PermissionActionName.Edit)] | ||
| public async Task<IActionResult> SetDefaultCurrency(string id) | ||
| { | ||
| var currency = await currencyService.GetCurrencyById(id); | ||
| if (currency == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotFound") }); | ||
|
|
||
| if (!currency.Published) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotPublished") }); | ||
|
|
||
| var storeId = CurrentStoreId; | ||
|
KrzysztofPajak marked this conversation as resolved.
|
||
|
|
||
| if (currency.LimitedToStores && !currency.Stores.Contains(storeId)) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotAssignedToStore") }); | ||
|
|
||
| var store = await storeService.GetStoreById(storeId); | ||
| if (store == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Stores.NotFound") }); | ||
|
|
||
| store.DefaultCurrencyId = currency.Id; | ||
| await storeService.UpdateStore(store); | ||
|
|
||
| return Json(new { success = true }); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [PermissionAuthorizeAction(PermissionActionName.Edit)] | ||
| public async Task<IActionResult> UnsetDefaultCurrency(string id) | ||
| { | ||
| var currency = await currencyService.GetCurrencyById(id); | ||
| if (currency == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotFound") }); | ||
|
|
||
| var storeId = CurrentStoreId; | ||
| var store = await storeService.GetStoreById(storeId); | ||
| if (store == null) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Stores.NotFound") }); | ||
|
|
||
| if (store.DefaultCurrencyId != currency.Id) | ||
| return Json(new { success = false, message = translationService.GetResource("Admin.Configuration.Currencies.NotDefaultCurrency") }); | ||
|
|
||
| store.DefaultCurrencyId = string.Empty; | ||
| await storeService.UpdateStore(store); | ||
|
|
||
| return Json(new { success = true }); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.