diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml new file mode 100644 index 000000000..566cc6135 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml @@ -0,0 +1,36 @@ +@model MessageTemplateModel +@{ + ViewBag.Title = Loc["Admin.Content.MessageTemplates.AddNew"]; + Layout = Constants.LayoutStore; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates.AddNew"] + + @Html.ActionLink(Loc["Admin.Content.MessageTemplates.BackToList"], "List") + +
+
+
+ + +
+
+
+
+ +
+
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml new file mode 100644 index 000000000..234ab9549 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml @@ -0,0 +1,41 @@ +@model MessageTemplateModel +@{ + ViewBag.Title = Loc["Admin.Content.MessageTemplates.EditMessageTemplateDetails"]; + Layout = Constants.LayoutStore; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates.EditMessageTemplateDetails"] - @Model.Name + + @Html.ActionLink(Loc["Admin.Content.MessageTemplates.BackToList"], "List") + +
+
+
+ + + + + @Loc["Admin.Common.Delete"] + +
+
+
+
+ +
+
+
+
+
+ diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml new file mode 100644 index 000000000..7aedc8cc8 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml @@ -0,0 +1,143 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + ViewBag.Title = Loc["Admin.Content.MessageTemplates"]; + Layout = Constants.LayoutStore; +} + +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates"] +
+ +
+
+ + + + +
+
+
+ + +
+
+
+
+
+
+
+
+
+ diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml new file mode 100644 index 000000000..8e3520ab3 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml @@ -0,0 +1,212 @@ +@using Grand.Domain.Messages +@using Microsoft.AspNetCore.Mvc.Razor +@using Constants = Grand.SharedUIResources.Constants +@model MessageTemplateModel + +
+ + + + + +@{ + Func + template = @
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ @foreach (var token in Model.AllowedTokens) + { + + } +
+ + +
+
+
+ +
+ + +
+
+ +
; +} + +
+
+
+ +
+ @if (!string.IsNullOrEmpty(Model.Id)) + { + + + } + else + { + + } +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ @foreach (var token in Model.AllowedTokens) + { + + } +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ + +
+
+
+
+
+ diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs new file mode 100644 index 000000000..30396d8f4 --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs @@ -0,0 +1,303 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.SharedKernel; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Models.Messages; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Store.Controllers; + +[PermissionAuthorize(PermissionSystemName.MessageTemplates)] +public class MessageTemplateController( + IMessageTemplateService messageTemplateService, + IEmailAccountService emailAccountService, + ILanguageService languageService, + ITranslationService translationService, + IMessageTokenProvider messageTokenProvider, + IDownloadService downloadService, + IContextAccessor contextAccessor) : BaseStoreController +{ + private string CurrentStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + public IActionResult Index() + { + return RedirectToAction("List"); + } + + public IActionResult List() + { + return View(); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task ListGlobal(DataSourceRequest command) + { + var allTemplates = await messageTemplateService.GetAllMessageTemplates(""); + var globalTemplates = allTemplates + .Where(t => !t.LimitedToStores) + .ToList(); + + var total = globalTemplates.Count; + var items = globalTemplates + .Skip((command.Page - 1) * command.PageSize) + .Take(command.PageSize) + .Select(x => x.ToModel()) + .ToList(); + + var gridModel = new DataSourceResult { + Data = items, + Total = total + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task ListStore(DataSourceRequest command) + { + var allTemplates = await messageTemplateService.GetAllMessageTemplates(CurrentStoreId); + var storeTemplates = allTemplates + .Where(t => t.LimitedToStores && t.Stores.Contains(CurrentStoreId)) + .ToList(); + + var total = storeTemplates.Count; + var items = storeTemplates + .Skip((command.Page - 1) * command.PageSize) + .Take(command.PageSize) + .Select(x => x.ToModel()) + .ToList(); + + var gridModel = new DataSourceResult { + Data = items, + Total = total + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create() + { + var model = new MessageTemplateModel { + AllowedTokens = messageTokenProvider.GetListOfAllowedTokens() + }; + + foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId)) + model.AvailableEmailAccounts.Add(ea.ToModel()); + + await AddLocales(languageService, model.Locales); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Create(MessageTemplateModel model, bool continueEditing) + { + if (ModelState.IsValid) + { + // Prevent duplicate: check only for store-specific templates with this name for the current store. + // GetMessageTemplateByName uses ACL and returns global templates too, so we use GetAllMessageTemplates("") + // and filter explicitly to avoid false positives on global templates sharing the same name. + var allTemplates = await messageTemplateService.GetAllMessageTemplates(""); + var existingStoreTemplate = allTemplates.FirstOrDefault(t => + t.Name == model.Name && + t.LimitedToStores && + t.Stores.Contains(CurrentStoreId)); + if (existingStoreTemplate != null) + { + ModelState.AddModelError("Name", translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists")); + model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId); + model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens(); + foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId)) + model.AvailableEmailAccounts.Add(ea.ToModel()); + return View(model); + } + + var messageTemplate = model.ToEntity(); + if (!model.HasAttachedDownload) + messageTemplate.AttachedDownloadId = ""; + if (model.SendImmediately) + messageTemplate.DelayBeforeSend = null; + + // Assign to the current store + messageTemplate.LimitedToStores = true; + messageTemplate.Stores = [CurrentStoreId]; + + await messageTemplateService.InsertMessageTemplate(messageTemplate); + + Success(translationService.GetResource("Admin.Content.MessageTemplates.AddNew")); + + if (continueEditing) + { + await SaveSelectedTabIndex(); + return RedirectToAction("Edit", new { id = messageTemplate.Id }); + } + + return RedirectToAction("List"); + } + + model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId); + model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens(); + foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId)) + model.AvailableEmailAccounts.Add(ea.ToModel()); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var messageTemplate = await messageTemplateService.GetMessageTemplateById(id); + if (messageTemplate == null) + return RedirectToAction("List"); + + // Only allow editing store-specific templates belonging to this store + if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId)) + return RedirectToAction("List"); + + var model = messageTemplate.ToModel(); + model.SendImmediately = !model.DelayBeforeSend.HasValue; + model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId); + model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens(); + + foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId)) + model.AvailableEmailAccounts.Add(ea.ToModel()); + + await AddLocales(languageService, model.Locales, (locale, languageId) => + { + locale.BccEmailAddresses = messageTemplate.GetTranslation(x => x.BccEmailAddresses, languageId, false); + locale.Subject = messageTemplate.GetTranslation(x => x.Subject, languageId, false); + locale.Body = messageTemplate.GetTranslation(x => x.Body, languageId, false); + locale.EmailAccountId = messageTemplate.GetTranslation(x => x.EmailAccountId, languageId, false); + }); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Edit(MessageTemplateModel model, bool continueEditing) + { + var messageTemplate = await messageTemplateService.GetMessageTemplateById(model.Id); + if (messageTemplate == null) + return RedirectToAction("List"); + + if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId)) + return RedirectToAction("List"); + + var prevAttachment = messageTemplate.AttachedDownloadId; + + if (ModelState.IsValid) + { + messageTemplate = model.ToEntity(messageTemplate); + if (!model.HasAttachedDownload) + messageTemplate.AttachedDownloadId = ""; + if (model.SendImmediately) + messageTemplate.DelayBeforeSend = null; + + // Keep it assigned to the current store + messageTemplate.LimitedToStores = true; + messageTemplate.Stores = [CurrentStoreId]; + + if (!string.IsNullOrEmpty(prevAttachment) && prevAttachment != messageTemplate.AttachedDownloadId) + { + var attachment = await downloadService.GetDownloadById(prevAttachment); + if (attachment != null) + await downloadService.DeleteDownload(attachment); + } + + await messageTemplateService.UpdateMessageTemplate(messageTemplate); + + Success(translationService.GetResource("Admin.Content.MessageTemplates.Updated")); + + if (continueEditing) + { + await SaveSelectedTabIndex(); + return RedirectToAction("Edit", new { id = messageTemplate.Id }); + } + + return RedirectToAction("List"); + } + + model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId); + model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens(); + foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId)) + model.AvailableEmailAccounts.Add(ea.ToModel()); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(string id) + { + var messageTemplate = await messageTemplateService.GetMessageTemplateById(id); + if (messageTemplate == null) + return RedirectToAction("List"); + + if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId)) + return RedirectToAction("List"); + + await messageTemplateService.DeleteMessageTemplate(messageTemplate); + + if (!string.IsNullOrEmpty(messageTemplate.AttachedDownloadId)) + { + var attachment = await downloadService.GetDownloadById(messageTemplate.AttachedDownloadId); + if (attachment != null) + await downloadService.DeleteDownload(attachment); + } + + Success(translationService.GetResource("Admin.Content.MessageTemplates.Deleted")); + return RedirectToAction("List"); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task CopyTemplate(MessageTemplateModel model) + { + var messageTemplate = await messageTemplateService.GetMessageTemplateById(model.Id); + if (messageTemplate == null) + return RedirectToAction("List"); + + // Only allow copying global templates (LimitedToStores = false) + if (messageTemplate.LimitedToStores) + return RedirectToAction("List"); + + // Prevent duplicate: check if a store-specific template with the same name already exists for the current store + var existing = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId); + if (existing != null) + { + Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists")); + return RedirectToAction("List"); + } + + try + { + var newMessageTemplate = await messageTemplateService.CopyMessageTemplate(messageTemplate); + // Assign copy to the current store + newMessageTemplate.LimitedToStores = true; + newMessageTemplate.Stores = [CurrentStoreId]; + await messageTemplateService.UpdateMessageTemplate(newMessageTemplate); + + Success(translationService.GetResource("Admin.Content.MessageTemplates.Copied")); + return RedirectToAction("Edit", new { id = newMessageTemplate.Id }); + } + catch (GrandException exc) + { + Error(exc.Message); + return RedirectToAction("List"); + } + } +} diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 4ed1399d6..527aba66d 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ