Skip to content
Merged
Show file tree
Hide file tree
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 May 24, 2026
ea6e230
Fix review feedback: localize error messages, fix duplicate loc key i…
Copilot May 24, 2026
61e3852
Hide unpublished currencies from store owner currency list
Copilot May 24, 2026
81277a7
Fix antiforgery token rendering and add missing resource strings
Copilot May 24, 2026
444b4c2
Convert AssignStore/UnassignStore to AJAX with grid refresh
Copilot May 24, 2026
2c90d7f
Mark assigned currencies with green badge in Name column
Copilot May 24, 2026
484e172
Add SetDefaultCurrency action and button for store owner default curr…
Copilot May 24, 2026
b9c7b4c
Validate currency is assigned to store before setting as default
Copilot May 24, 2026
291b96d
Add dedicated IsDefaultStoreCurrency column with Set as Default button
Copilot May 24, 2026
a8ed5da
Add UnsetDefaultCurrency action and button in IsDefaultStoreCurrency …
Copilot May 24, 2026
4888e49
Add GetAllPublishedCurrencies to ICurrencyService and use in Store Cu…
Copilot May 24, 2026
16e9a55
Revert "Add GetAllPublishedCurrencies to ICurrencyService and use in …
KrzysztofPajak May 24, 2026
a9a9647
Potential fix for pull request finding
KrzysztofPajak May 24, 2026
6251ed3
Allow global (non-limited) currencies to be set as store default
Copilot May 24, 2026
87a13ac
Disable serverPaging/serverSorting/serverFiltering; remove unused Dat…
Copilot May 24, 2026
e09954f
Block UnassignStore when currency is store default
Copilot May 24, 2026
46908c4
Add published guard in SetDefaultCurrency to reject unpublished curre…
Copilot May 24, 2026
5628171
Update css
KrzysztofPajak May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6986,7 +6986,6 @@ h2 {
color: #f9f9f9;
}
[data-theme="dark"] .k-grid table a, [data-theme="dark"] .premium-list li span {
color: #A5D8FE;
font-weight: 500;
}
[data-theme="dark"] .toggle a span {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7094,7 +7094,6 @@ table.tableFixHead {
color: #f9f9f9;
}
[data-theme="dark"] .k-grid table a, [data-theme="dark"] .premium-list li span {
color: #A5D8FE;
font-weight: 500;
}
[data-theme="dark"] .toggle a span {
Expand Down
216 changes: 216 additions & 0 deletions src/Web/Grand.Web.Store/Areas/Store/Views/Currency/List.cshtml
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 src/Web/Grand.Web.Store/Controllers/CurrencyController.cs
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);

Comment thread
KrzysztofPajak marked this conversation as resolved.
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
})
Comment thread
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 });
Comment thread
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;
Comment thread
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 });
}
}
Loading
Loading