瀏覽代碼

Mail-Benachrichtigung beinahe abgeschlossen!

Arne Diekmann 8 年之前
父節點
當前提交
7e64aff6ac
共有 25 個文件被更改,包括 1177 次插入64 次删除
  1. 13 0
      GreenTree.Nachtragsmanagement.Core/Domain/Misc/MailNotification.cs
  2. 0 1
      GreenTree.Nachtragsmanagement.Core/Domain/Site/Site.cs
  3. 20 12
      GreenTree.Nachtragsmanagement.Services/Configuration/ConfigurationService.cs
  4. 10 2
      GreenTree.Nachtragsmanagement.Services/Configuration/IConfigurationService.cs
  5. 2 2
      GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj
  6. 11 0
      GreenTree.Nachtragsmanagement.Services/Misc/INotificationService.cs
  7. 38 3
      GreenTree.Nachtragsmanagement.Services/Misc/NotificationService.cs
  8. 1 1
      GreenTree.Nachtragsmanagement.Services/packages.config
  9. 4 2
      GreenTree.Nachtragsmanagement.Web.Framework/ApplicationContext.cs
  10. 32 0
      GreenTree.Nachtragsmanagement.Web/App_Start/FunctionConfig.cs
  11. 14 0
      GreenTree.Nachtragsmanagement.Web/App_Start/RouteConfig.cs
  12. 二進制
      GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-32-contrast.png
  13. 二進制
      GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-32.png
  14. 二進制
      GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-MailNotifications-32-contrast.png
  15. 二進制
      GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-MailNotifications-32.png
  16. 243 0
      GreenTree.Nachtragsmanagement.Web/Controllers/MiscController.cs
  17. 152 3
      GreenTree.Nachtragsmanagement.Web/Extensions/GridViewSettingsHelper.cs
  18. 31 1
      GreenTree.Nachtragsmanagement.Web/Global.asax.cs
  19. 10 0
      GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj
  20. 105 0
      GreenTree.Nachtragsmanagement.Web/Models/Misc/MailNotificationDataModel.cs
  21. 149 37
      GreenTree.Nachtragsmanagement.Web/Scheduling/AppendixNotificationPlugin.cs
  22. 114 0
      GreenTree.Nachtragsmanagement.Web/Views/Misc/MailNotifications.cshtml
  23. 193 0
      GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationEditPartial.cshtml
  24. 9 0
      GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationGridPartial.cshtml
  25. 26 0
      GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationPluginJobsPartial.cshtml

+ 13 - 0
GreenTree.Nachtragsmanagement.Core/Domain/Misc/MailNotification.cs

@@ -40,5 +40,18 @@ namespace GreenTree.Nachtragsmanagement.Core.Domain.Misc
             get { return _users ?? ( _users = new List<User.User>()); }
             protected set { _users = value; }
         }
+
+        #region Helper
+
+        /// <summary>
+        /// Adds missing users and removes not selected users
+        /// </summary>
+        /// <param name="users">Site users.</param>
+        public void SetUsers(ICollection<User.User> users)
+        {
+            Users = users;
+        }
+
+        #endregion
     }
 }

+ 0 - 1
GreenTree.Nachtragsmanagement.Core/Domain/Site/Site.cs

@@ -104,7 +104,6 @@ namespace GreenTree.Nachtragsmanagement.Core.Domain.Site
             Appendices = appendices;
         }
 
-
         /// <summary>
         /// Adds missing users and removes not selected users
         /// </summary>

+ 20 - 12
GreenTree.Nachtragsmanagement.Services/Configuration/ConfigurationService.cs

@@ -119,34 +119,42 @@ namespace GreenTree.Nachtragsmanagement.Services.Configuration
         /// </summary>
         /// <typeparam name="T">The actual type.</typeparam>
         /// <param name="configItem">ConfigItem.</param>
-        /// <param name="success">Determines if the conversion was possible.</param>
-        public T TryGetConfigItemValue<T>(ConfigItem configItem, out bool success)
+        /// <param name="defaultValue">Value, when conversion fails.</param>
+        public T TryGetConfigItemValue<T>(ConfigItem configItem, T defaultValue)
         {
-            success = false;
-
             if (configItem == null)
-            {
-
-
-                return default(T);
-            }
+                return defaultValue;
 
             object result = null;
 
             try
             {
                 result = Convert.ChangeType(configItem.Value, typeof(T));
-
-                success = true;
             }
             catch
             {
-                success = false;
+                result = defaultValue;
             }
 
             return (T)result;
         }
 
+        /// <summary>
+        /// Trys to convert the config items value type and returns its actual value, otherwise NULL
+        /// </summary>
+        /// <typeparam name="T">The actual type.</typeparam>
+        /// <param name="configItemName">ConfigItem name.</param>
+        /// <param name="defaultValue">Value, when conversion fails.</param>
+        public T TryGetConfigItemValue<T>(string configItemName, T defaultValue)
+        {
+            if (String.IsNullOrEmpty(configItemName))
+                return defaultValue;
+
+            var configItem = GetConfigItemByName(configItemName);
+
+            return TryGetConfigItemValue(configItem, defaultValue);
+        }
+
         #endregion
     }
 }

+ 10 - 2
GreenTree.Nachtragsmanagement.Services/Configuration/IConfigurationService.cs

@@ -66,8 +66,16 @@ namespace GreenTree.Nachtragsmanagement.Services.Configuration
         /// </summary>
         /// <typeparam name="T">The actual type.</typeparam>
         /// <param name="configItem">ConfigItem.</param>
-        /// <param name="success">Determines if the conversion was possible.</param>
-        T TryGetConfigItemValue<T>(ConfigItem configItem, out bool success);
+        /// <param name="defaultValue">Value, when conversion fails.</param>
+        T TryGetConfigItemValue<T>(ConfigItem configItem, T defaultValue);
+
+        /// <summary>
+        /// Trys to convert the config items value type and returns its actual value, otherwise NULL
+        /// </summary>
+        /// <typeparam name="T">The actual type.</typeparam>
+        /// <param name="configItemName">ConfigItem name.</param>
+        /// <param name="defaultValue">Value, when conversion fails.</param>
+        T TryGetConfigItemValue<T>(string configItemName, T defaultValue);
 
         #endregion
     }

+ 2 - 2
GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj

@@ -30,8 +30,8 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="Autofac, Version=4.6.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
-      <HintPath>..\packages\Autofac.4.6.1\lib\net45\Autofac.dll</HintPath>
+    <Reference Include="Autofac, Version=4.0.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
+      <HintPath>..\packages\Autofac.4.0.1\lib\net45\Autofac.dll</HintPath>
     </Reference>
     <Reference Include="Common.Logging, Version=3.3.1.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
       <HintPath>..\packages\Common.Logging.3.3.1\lib\net40\Common.Logging.dll</HintPath>

+ 11 - 0
GreenTree.Nachtragsmanagement.Services/Misc/INotificationService.cs

@@ -9,9 +9,20 @@ namespace GreenTree.Nachtragsmanagement.Services.Misc
 {
     public interface INotificationService
     {
+        /// <summary>
+        /// Loads all implementations of the INotificationPlugin
+        /// </summary>
+        IEnumerable<INotificationPlugin> LoadNotificationPlugins();
+
         /// <summary>
         /// Searches for all implementations of the INotificationPlugin
         /// </summary>
         IEnumerable<INotificationPlugin> GetNotificationPlugins();
+
+        /// <summary>
+        /// Gets a notification plugin by a specific name
+        /// </summary>
+        /// <param name="pluginSystemName">SystemName of notification plugin.</param>
+        INotificationPlugin GetNotificationPlugin(string pluginSystemName);
     }
 }

+ 38 - 3
GreenTree.Nachtragsmanagement.Services/Misc/NotificationService.cs

@@ -1,4 +1,6 @@
-using GreenTree.Nachtragsmanagement.Core.Plugins;
+using Autofac;
+using GreenTree.Nachtragsmanagement.Core;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -11,9 +13,9 @@ namespace GreenTree.Nachtragsmanagement.Services.Misc
     public class NotificationService : INotificationService
     {
         /// <summary>
-        /// Searches for all implementations of the INotificationPlugin
+        /// Loads all implementations of the INotificationPlugin
         /// </summary>
-        public IEnumerable<INotificationPlugin> GetNotificationPlugins()
+        public IEnumerable<INotificationPlugin> LoadNotificationPlugins()
         {
             var type = typeof(INotificationPlugin);
             var assemblies = AppDomain.CurrentDomain.GetAssemblies();
@@ -27,5 +29,38 @@ namespace GreenTree.Nachtragsmanagement.Services.Misc
 
             return types;
         }
+
+        /// <summary>
+        /// Searches for all implementations of the INotificationPlugin
+        /// </summary>
+        public IEnumerable<INotificationPlugin> GetNotificationPlugins()
+        {
+            var notificationPlugins = Singleton<IContainer>.Instance.Resolve<IEnumerable<INotificationPlugin>>();
+
+            if (notificationPlugins != null)
+                return notificationPlugins;
+
+            return LoadNotificationPlugins();
+        }
+
+        /// <summary>
+        /// Gets a notification plugin by a specific name
+        /// </summary>
+        /// <param name="pluginSystemName">SystemName of notification plugin.</param>
+        public INotificationPlugin GetNotificationPlugin(string pluginSystemName)
+        {
+            var notificationPlugin = Singleton<IContainer>.Instance.ResolveNamed<INotificationPlugin>(pluginSystemName);
+
+            if (notificationPlugin != null)
+                return notificationPlugin;
+
+            var notificationPlugins = GetNotificationPlugins();
+
+            if (notificationPlugins == null)
+                return null;
+
+            return notificationPlugins
+                .FirstOrDefault(p => p.SystemName == pluginSystemName);
+        }
     }
 }

+ 1 - 1
GreenTree.Nachtragsmanagement.Services/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Autofac" version="4.6.1" targetFramework="net452" />
+  <package id="Autofac" version="4.0.1" targetFramework="net452" />
   <package id="Common.Logging" version="3.3.1" targetFramework="net452" />
   <package id="Common.Logging.Core" version="3.3.1" targetFramework="net452" />
   <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net452" />

+ 4 - 2
GreenTree.Nachtragsmanagement.Web.Framework/ApplicationContext.cs

@@ -144,11 +144,13 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
             // Register notification plugin types
             var notificationPluginBuilder = new ContainerBuilder();
             var notificationPluginService = _appContainer.Resolve<INotificationService>();
-            var notificationPlugins = notificationPluginService.GetNotificationPlugins();
+            var notificationPlugins = notificationPluginService.LoadNotificationPlugins();
 
             foreach (var notificationPlugin in notificationPlugins)
             {
-                notificationPluginBuilder.RegisterType(notificationPlugin.GetType()).As<INotificationPlugin>();
+                notificationPluginBuilder.RegisterType(notificationPlugin.GetType())
+                    .Named<INotificationPlugin>(notificationPlugin.SystemName)
+                    .As<INotificationPlugin>();
             }
 
             notificationPluginBuilder.Update(_appContainer);

+ 32 - 0
GreenTree.Nachtragsmanagement.Web/App_Start/FunctionConfig.cs

@@ -284,6 +284,38 @@ namespace GreenTree.Nachtragsmanagement.Web.App_Start
                     IsMenuMember = false,
                     Plugin = "System"
                 },
+                new Function
+                {
+                    Name = "Misc",
+                    Description = "Sonstiges",
+                    ImageUrl = "~/Content/Images/function-Misc-32.png",
+                    IsMenuMember = true,
+                    Plugin = "System"
+                },
+                new Function
+                {
+                    Name = "Misc-MailNotifications",
+                    Description = "Benachrichtigungen",
+                    ImageUrl = "~/Content/Images/function-Misc-MailNotifications-32.png",
+                    GroupName = "Misc",
+                    RouteName = "GreenTree.Nachtragsmanagement.Web.Misc.MailNotifications",
+                    IsMenuMember = true,
+                    Plugin = "System",
+                    BaseWidth = 900,
+                    MinWidth = 700,
+                    BaseHeight = 550,
+                    MinHeight = 450,
+                    AllowMaximize = true,
+                    MaximizedOnStart = false
+                },
+                new Function
+                {
+                    Name = "Misc-MailNotifications-Edit",
+                    Description = "Benachrichtigungen editieren",
+                    GroupName = "Misc-MailNotifications",
+                    IsMenuMember = false,
+                    Plugin = "System"
+                },
             };
         }
     }

+ 14 - 0
GreenTree.Nachtragsmanagement.Web/App_Start/RouteConfig.cs

@@ -131,6 +131,20 @@ namespace GreenTree.Nachtragsmanagement.Web
                    "GreenTree.Nachtragsmanagement.Web.Controllers"
                }
             );
+
+            routes.MapRoute(
+                "GreenTree.Nachtragsmanagement.Web.Misc.MailNotifications",
+                "misc/viewmailnotifications",
+               new
+               {
+                   controller = "Misc",
+                   action = "ViewMailNotifications"
+               },
+               new[]
+               {
+                    "GreenTree.Nachtragsmanagement.Web.Controllers"
+               }
+            );
         }
     }
 }

二進制
GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-32-contrast.png


二進制
GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-32.png


二進制
GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-MailNotifications-32-contrast.png


二進制
GreenTree.Nachtragsmanagement.Web/Content/Images/function-Misc-MailNotifications-32.png


+ 243 - 0
GreenTree.Nachtragsmanagement.Web/Controllers/MiscController.cs

@@ -0,0 +1,243 @@
+using DevExpress.Utils;
+using DevExpress.Web;
+using DevExpress.Web.ASPxThemes;
+using DevExpress.Web.Mvc;
+using DevExpress.XtraPrinting;
+using GreenTree.Nachtragsmanagement.Core.Domain.Deviation;
+using GreenTree.Nachtragsmanagement.Core.Domain.User;
+using GreenTree.Nachtragsmanagement.Services.Appendix;
+using GreenTree.Nachtragsmanagement.Services.Deviation;
+using GreenTree.Nachtragsmanagement.Services.Misc;
+using GreenTree.Nachtragsmanagement.Services.User;
+using GreenTree.Nachtragsmanagement.Web.Framework.Authorization;
+using GreenTree.Nachtragsmanagement.Web.Models.Global;
+using GreenTree.Nachtragsmanagement.Web.Models.Misc;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.UI;
+using System.Web.UI.WebControls;
+
+namespace GreenTree.Nachtragsmanagement.Web.Controllers
+{
+    public class MiscController : Controller
+    {
+        private readonly IMiscService _miscService;
+        private readonly IUserService _userService;
+        private readonly INotificationService _notificationService;
+
+        public MiscController(
+            IMiscService miscService,
+            IUserService userService,
+            INotificationService notificationService)
+        {
+            _miscService = miscService;
+            _userService = userService;
+            _notificationService = notificationService;
+
+            ViewData["AllUsers"] = _userService.GetAllUsers();
+            ViewData["AllUsersWithRole"] =
+                _userService.GetAllUsers()
+                    .Select(u => new
+                    {
+                        Id = u.Id,
+                        Description = String.Format("{0} - {1}", u.Lastname,
+                            String.Join(", ", u.Roles
+                                .Select(r => r.Description)))
+                    })
+                    .ToList();
+
+            ViewData["AllNotificationPlugins"] =
+                _notificationService.GetNotificationPlugins();
+        }
+
+        #region MailNotifications
+
+        /// <summary>
+        /// Basic mailNotification view function
+        /// </summary>
+        [FunctionAuthorize(true, "Misc-MailNotifications")]
+        public ActionResult ViewMailNotifications()
+        {
+            var mailNotifications = _miscService.GetAllMailNotifications();
+            var mailNotificationModels = mailNotifications
+                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .ToList();
+
+            return View("~/Views/Misc/MailNotifications.cshtml", mailNotificationModels);
+        }
+
+        /// <summary>
+        /// Get JSON data of specific mailNotification
+        /// </summary>
+        /// <param name="id">MailNotification id.</param>
+        public ActionResult GetMailNotification(int id = -1)
+        {
+            var mailNotification = _miscService.GetMailNotificationById(id);
+            if (mailNotification == null)
+                return new JsonResult
+                {
+                    Data = "notFound",
+                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
+                };
+
+            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(mailNotification, false, _notificationService);
+            
+            return new JsonResult
+            {
+                Data = JsonConvert.SerializeObject(mailNotificationModel),
+                JsonRequestBehavior = JsonRequestBehavior.AllowGet
+            };
+        }
+
+        /// <summary>
+        /// Callback result for mailNotification grid
+        /// </summary>
+        /// <param name="scrollHeight">The height of the grid scrollable component.</param>
+        public ActionResult PartialMailNotifications(int scrollHeight = -1)
+        {
+            var mailNotifications = _miscService.GetAllMailNotifications();
+            var mailNotificationModels = mailNotifications
+                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .ToList();
+
+            ViewData["ScrollHeight"] = scrollHeight;
+
+            return PartialView("~/Views/Misc/_MailNotificationGridPartial.cshtml", mailNotificationModels);
+        }
+
+        /// <summary>
+        /// Callback result for mailNotification job combobox
+        /// </summary>
+        /// <param name="pluginSystemName">The system name of the corresponding notification plugin.</param>
+        public ActionResult PartialNotificationPluginJobs(string pluginSystemName)
+        {
+            var notificationPlugin = _notificationService.GetNotificationPlugin(pluginSystemName);
+
+            if (notificationPlugin == null)
+                return PartialView("~/Views/Misc/_MailNotificationPluginJobsPartial.cshtml", null);
+
+            var mailNotificationModel = new MailNotificationDataModel
+            {
+                NotificationPlugin = notificationPlugin
+            };
+
+            return PartialView("~/Views/Misc/_MailNotificationPluginJobsPartial.cshtml", mailNotificationModel);
+        }
+
+        /// <summary>
+        /// Export result for mailNotification grid
+        /// </summary>
+        [HttpPost]
+        public ActionResult ExportPartialMailNotifications(GridViewExportFormat exportformat)
+        {
+            if (exportformat == null || String.IsNullOrEmpty(exportformat.Format))
+                return new EmptyResult();
+
+            var mailNotifications = _miscService.GetAllMailNotifications();
+            var mailNotificationModels = mailNotifications
+                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .ToList();
+
+            var viewContext = new ViewContext();
+            var viewPage = new ViewPage();
+            var htmlHelper = new HtmlHelper(viewContext, viewPage);
+
+            var gridViewSettings = Extensions.GridViewSettingsHelper.MailNotificationGridViewSettings(htmlHelper);
+
+            switch (exportformat.Format.ToLower())
+            {
+                case "xlsx":
+                    return GridViewExtension.ExportToXlsx(gridViewSettings, mailNotificationModels);
+                case "xls":
+                    return GridViewExtension.ExportToXls(gridViewSettings, mailNotificationModels);
+                case "pdf":
+                    return GridViewExtension.ExportToPdf(gridViewSettings, mailNotificationModels);
+                default:
+                    return new EmptyResult();
+            }
+        }
+
+        /// <summary>
+        /// Partial edit for editing of existing or for new mailNotification
+        /// </summary>
+        /// <param name="id">Id for existing mailNotification, otherweise -1.</param>
+        public ActionResult EditMailNotification(int id = -1)
+        {
+            var mailNotification = _miscService.GetMailNotificationById(id);
+            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(mailNotification, true, _notificationService);
+
+            return PartialView("~/Views/Misc/_MailNotificationEditPartial.cshtml", mailNotificationModel);
+        }
+
+        /// <summary>
+        /// Partial edit result if ModelState is valid, otherwise simple JSON result for success
+        /// </summary>
+        /// <param name="mailNotificationModel">MailNotification model to be saved.</param>
+        [HttpPost, ValidateInput(false)]
+        public ActionResult EditMailNotification(MailNotificationDataModel mailNotificationModel)
+        {
+            if (!ModelState.IsValid)
+            {
+                foreach (var role in mailNotificationModel.UserValues)
+                    mailNotificationModel.UserDescriptions.Add(
+                        ((IList<User>)ViewData["AllUsers"])
+                            .First(r => r.Id == role).Lastname);
+
+                return PartialView("~/Views/Misc/_MailNotificationEditPartial.cshtml", mailNotificationModel);
+            }
+
+            var selectedUsers = _userService.GetUsersByIds(mailNotificationModel.UserValues.ToArray());
+
+            if (mailNotificationModel.Id == -1)
+            {
+                var mailNotification = mailNotificationModel.ToMailNotification();
+
+                mailNotification.SetUsers(selectedUsers);
+
+                _miscService.InsertMailNotification(mailNotification);
+            }
+            else
+            {
+                var mailNotification = _miscService.GetMailNotificationById(mailNotificationModel.Id);
+
+                mailNotification.CronExpression = mailNotificationModel.CronExpression;
+                mailNotification.NotificationPluginSystemName = mailNotificationModel.NotificationPluginSystemName;
+                mailNotification.NotificationJobSystemName = mailNotificationModel.NotificationJobSystemName;
+
+                mailNotification.SetUsers(selectedUsers);
+
+                _miscService.UpdateMailNotification(mailNotification);
+            }
+
+            return new JsonResult
+            {
+                Data = "success"
+            };
+        }
+
+        /// <summary>
+        /// Simple JSON result for deleting a specific mailNotification
+        /// </summary>
+        /// <param name="id">MailNotification id.</param>
+        [HttpPost]
+        public ActionResult DeleteMailNotification(int id)
+        {
+            var mailNotification = _miscService.GetMailNotificationById(id);
+
+            if (mailNotification != null)
+                _miscService.DeleteMailNotification(mailNotification);
+
+            return new JsonResult
+            {
+                Data = "success"
+            };
+        }
+
+        #endregion
+    }
+}

+ 152 - 3
GreenTree.Nachtragsmanagement.Web/Extensions/GridViewSettingsHelper.cs

@@ -29,7 +29,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
             s.Name = "devGridViewSite";
             s.KeyFieldName = "Id";
             s.CallbackRouteValues = new { Controller = "Site", Action = "PartialSites" };
-            s.Width = Unit.Percentage(100);
+            s.Width = Unit.Percentage(99);
             s.Settings.ShowFilterRow = true;
             s.Settings.ShowFilterRowMenu = true;
             s.Settings.ShowFooter = true;
@@ -224,7 +224,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
             s.Name = "devGridViewDeviation";
             s.KeyFieldName = "Id";
             s.CallbackRouteValues = new { Controller = "Deviation", Action = "PartialDeviations" };
-            s.Width = Unit.Percentage(100);
+            s.Width = Unit.Percentage(99);
             s.Settings.ShowFilterRow = true;
             s.Settings.ShowFilterRowMenu = true;
             s.Settings.ShowFooter = true;
@@ -457,7 +457,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
             s.Name = "devGridViewAppendix";
             s.KeyFieldName = "Id";
             s.CallbackRouteValues = new { Controller = "Appendix", Action = "PartialAppendices" };
-            s.Width = Unit.Percentage(100);
+            s.Width = Unit.Percentage(99);
             s.Settings.ShowFilterRow = true;
             s.Settings.ShowFilterRowMenu = true;
             s.Settings.ShowFooter = true;
@@ -670,5 +670,154 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
 
             return s;
         }
+
+        /// <summary>
+        /// Creates GridViewSettings for the mailNotifications gridView
+        /// </summary>
+        /// <param name="html">Current HtmlHelper context.</param>
+        public static GridViewSettings MailNotificationGridViewSettings(this System.Web.Mvc.HtmlHelper html)
+        {
+            var s = new GridViewSettings();
+
+            s.Name = "devGridViewMailNotifications";
+            s.KeyFieldName = "Id";
+            s.CallbackRouteValues = new { Controller = "Misc", Action = "PartialMailNotifications" };
+            s.Width = Unit.Percentage(99);
+            s.Settings.ShowFilterRow = true;
+            s.Settings.ShowFilterRowMenu = true;
+            s.Settings.ShowFooter = true;
+            s.Settings.ShowGroupPanel = true;
+            s.Settings.AutoFilterCondition = AutoFilterCondition.Contains;
+            s.Settings.VerticalScrollBarMode = ScrollBarMode.Auto;
+            s.Settings.VerticalScrollableHeight =
+                (html.ViewData["ScrollHeight"] == null || (int)html.ViewData["ScrollHeight"] == -1)
+                ? 400
+                : (int)html.ViewData["ScrollHeight"];
+            s.SettingsExport.Landscape = true;
+            s.SettingsExport.FileName = "Benachrichtigungsliste";
+            s.SettingsPopup.CustomizationWindow.Width = new Unit(250, UnitType.Pixel);
+            s.SettingsPopup.CustomizationWindow.Height = new Unit(350, UnitType.Pixel);
+            s.SettingsBehavior.EnableCustomizationWindow = true;
+
+            s.Toolbars.Add(t =>
+            {
+                var refreshItem = t.Items.Add(GridViewToolbarCommand.Refresh);
+                refreshItem.Text = "Aktualisieren";
+                var expandItem = t.Items.Add(GridViewToolbarCommand.FullExpand);
+                expandItem.Text = "Alle aufklappen";
+                var collapseItem = t.Items.Add(GridViewToolbarCommand.FullCollapse);
+                collapseItem.Text = "Alle einklappen";
+                var filterItem = t.Items.Add(GridViewToolbarCommand.ClearFilter);
+                filterItem.Text = "Filter entfernen";
+                t.Items.Add(i =>
+                {
+                    i.Text = "Spalten anzeigen / ausblenden";
+                    i.Name = "ToggleColumnChooser";
+                    i.Image.IconID = IconID.OtherViewgridlines16x16gray;
+                    i.BeginGroup = true;
+                });
+                t.Items.Add(i =>
+                {
+                    i.Text = "Exportieren nach";
+                    i.Image.IconID = IconID.ActionsDownload16x16office2013;
+                    i.BeginGroup = true;
+                    i.Items.Add(exportitem =>
+                    {
+                        exportitem.Name = "Pdf";
+                        exportitem.Text = "PDF";
+                        exportitem.Image.IconID = IconID.ExportExporttopdf16x16office2013;
+                    });
+                    i.Items.Add(exportitem =>
+                    {
+                        exportitem.Name = "Xlsx";
+                        exportitem.Text = "XLSX";
+                        exportitem.Image.IconID = IconID.ExportExporttoxlsx16x16office2013;
+                    });
+                    i.Items.Add(exportitem =>
+                    {
+                        exportitem.Name = "Xls";
+                        exportitem.Text = "XLS";
+                        exportitem.Image.IconID = IconID.ExportExporttoxls16x16office2013;
+                    });
+                });
+            });
+
+            if (_userContext.CurrentUser.HasFunction("Misc-MailNotifications-Edit"))
+            {
+                s.Columns.Add(column =>
+                {
+                    column.Caption = "#";
+                    column.SetDataItemTemplateContent(c =>
+                    {
+                        html.ViewContext.Writer.Write(
+                            "<a href=\"#\" onclick=\"editMailNotification(" + DataBinder.Eval(c.DataItem, "Id") + ")\">Bearbeiten</a>&nbsp;");
+                        html.ViewContext.Writer.Write(
+                            "<a href=\"#\" onclick=\"confirmDelete(" + DataBinder.Eval(c.DataItem, "Id") + ")\">Löschen</a>");
+                    });
+                    column.SetHeaderTemplateContent(c =>
+                    {
+                        html.ViewContext.Writer.Write(
+                            "<a href=\"#\" onclick=\"editMailNotification(-1)\">Neu</a>&nbsp;");
+                    });
+                    column.Settings.AllowDragDrop = DefaultBoolean.False;
+                    column.Settings.AllowSort = DefaultBoolean.False;
+                    column.Width = new Unit(150, UnitType.Pixel);
+                });
+            }
+            s.Columns.Add(column =>
+            {
+                column.Caption = "Zeitplan";
+                column.FieldName = "CronExpressionDescription";
+                column.MinWidth = 100;
+                column.Width = new Unit(20, UnitType.Percentage);
+            });
+            s.Columns.Add(column =>
+            {
+                column.Caption = "Benachrichtigungs-Plugin";
+                column.FieldName = "NotificationPluginSystemNameDescription";
+                column.Width = new Unit(17.5, UnitType.Percentage);
+            });
+            s.Columns.Add(column =>
+            {
+                column.Caption = "Benachrichtigungs-Job";
+                column.FieldName = "NotificationJobSystemNameDescription";
+                column.PropertiesEdit.DisplayFormatString = "dd.MM.yyyy";
+                column.MinWidth = 110;
+                column.Width = new Unit(17.5, UnitType.Percentage);
+            });
+            s.Columns.Add(column =>
+            {
+                column.Caption = "Mitarbeiter";
+                column.MinWidth = 150;
+                column.Width = new Unit(30, UnitType.Percentage);
+                column.SetDataItemTemplateContent(c =>
+                {
+                    var userDescriptions = DataBinder.Eval(c.DataItem, "UserDescriptions") as List<string>;
+
+                    if (userDescriptions != null)
+                    {
+                        html.ViewContext.Writer.Write(
+                            String.Join("<br />", userDescriptions));
+                    }
+                });
+            });
+
+            s.ClientLayout = (sender, e) =>
+            {
+                if (e.LayoutMode == ClientLayoutMode.Loading)
+                {
+                    if (System.Web.HttpContext.Current.Session["MailNotificationsGridState"] != null)
+                        e.LayoutData = (string)System.Web.HttpContext.Current.Session["MailNotificationsGridState"];
+                }
+                else
+                    System.Web.HttpContext.Current.Session["MailNotificationsGridState"] = e.LayoutData;
+            };
+            s.ClientSideEvents.BeginCallback = "function (s, e) { e.customArgs['scrollHeight'] = [ gridScrollHeight ]; }";
+            s.ClientSideEvents.ToolbarItemClick = "function (s, e) { onToolbarItemClick(s, e); }";
+
+            s.Styles.AlternatingRow.BackColor = System.Drawing.Color.FromArgb(247, 247, 247);
+
+            return s;
+        }
     }
 }

+ 31 - 1
GreenTree.Nachtragsmanagement.Web/Global.asax.cs

@@ -629,11 +629,41 @@ namespace GreenTree.Nachtragsmanagement.Web
                         Value = "56"
                     },
                     new ConfigItem
+                    {
+                        Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.StateCondition",
+                        TypeFullName = "System.Int32",
+                        Value = states[0].Id.ToString()
+                    },
+                    new ConfigItem
                     {
                         Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.StateSet",
                         TypeFullName = "System.Int32",
                         Value = states[1].Id.ToString()
-                    }
+                    },
+                    new ConfigItem
+                    {
+                        Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.Interval",
+                        TypeFullName = "System.Int32",
+                        Value = "2"
+                    },
+                    new ConfigItem
+                    {
+                        Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.AgeDays",
+                        TypeFullName = "System.Int32",
+                        Value = "14"
+                    },
+                    new ConfigItem
+                    {
+                        Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.StateCondition",
+                        TypeFullName = "System.Int32",
+                        Value = states[2].Id.ToString()
+                    },
+                    new ConfigItem
+                    {
+                        Name = "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.Interval",
+                        TypeFullName = "System.Int32",
+                        Value = "2"
+                    },
                 };
 
                 foreach (var configItem in configItems)

+ 10 - 0
GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj

@@ -187,8 +187,12 @@
     <Content Include="Content\Images\function-Administration-Users-32-contrast.png" />
     <Content Include="Content\Images\function-Administration-Users-32.png" />
     <Content Include="Content\Images\function-Administration-Plugins-32.png" />
+    <Content Include="Content\Images\function-Misc-32-contrast.png" />
+    <Content Include="Content\Images\function-Misc-MailNotifications-32-contrast.png" />
     <Content Include="Content\Images\function-Site-Sites-32-contrast.png" />
     <Content Include="Content\Images\function-Site-Sites-32.png" />
+    <Content Include="Content\Images\function-Misc-32.png" />
+    <Content Include="Content\Images\function-Misc-MailNotifications-32.png" />
     <Content Include="Content\Images\maximize-16.png" />
     <Content Include="Content\Images\minimize-16.png" />
     <Content Include="Content\Images\function-Appendix-Claims-32.png" />
@@ -296,6 +300,10 @@
     <Content Include="Views\Shared\DataEditorTemplates\_CategoriesComboBox.cshtml" />
     <Content Include="Views\Appendices\_InvoiceEditPartial.cshtml" />
     <Content Include="Views\Appendices\_InvoiceListPartial.cshtml" />
+    <Content Include="Views\Misc\MailNotifications.cshtml" />
+    <Content Include="Views\Misc\_MailNotificationGridPartial.cshtml" />
+    <Content Include="Views\Misc\_MailNotificationEditPartial.cshtml" />
+    <Content Include="Views\Misc\_MailNotificationPluginJobsPartial.cshtml" />
     <None Include="Web.Debug.config">
       <DependentUpon>Web.config</DependentUpon>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -311,6 +319,8 @@
     <Compile Include="App_Start\FunctionConfig.cs" />
     <Compile Include="App_Start\RouteConfig.cs" />
     <Compile Include="App_Start\WebApiConfig.cs" />
+    <Compile Include="Controllers\MiscController.cs" />
+    <Compile Include="Models\Misc\MailNotificationDataModel.cs" />
     <Compile Include="Scheduling\AppendixNotificationPlugin.cs" />
     <Compile Include="Scheduling\JobScheduler.cs" />
     <Compile Include="Scheduling\JobWorker.cs" />

+ 105 - 0
GreenTree.Nachtragsmanagement.Web/Models/Misc/MailNotificationDataModel.cs

@@ -0,0 +1,105 @@
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using GreenTree.Nachtragsmanagement.Services.Misc;
+using GreenTree.Nachtragsmanagement.Web.Models.Admin.User;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Web.Models.Misc
+{
+    public class MailNotificationDataModel
+    {
+        public int Id { get; set; }
+        public string CronExpression { get; set; }
+        public string CronExpressionDescription { get; set; }
+        public INotificationPlugin NotificationPlugin { get; set; }
+        public string NotificationPluginSystemName { get; set; }
+        public string NotificationPluginSystemNameDescription { get; set; }
+        public string NotificationJobSystemName { get; set; }
+        public string NotificationJobSystemNameDescription { get; set; }
+        public ICollection<int> UserValues { get; set; }
+        public ICollection<string> UserDescriptions { get; set; }
+        public ICollection<UserDataModel> Users { get; set; }
+        public string UserDescription
+        {
+            get
+            {
+                if (UserDescriptions == null)
+                    return String.Empty;
+                else
+                    return String.Join(", ", UserDescriptions);
+            }
+        }
+
+        public MailNotificationDataModel()
+        {
+            UserValues = new List<int>();
+            UserDescriptions = new List<string>();
+        }
+
+        public static MailNotificationDataModel FromMailNotification(Core.Domain.Misc.MailNotification mailNotificationEntity, 
+            bool newWhenIsNull, INotificationService service)
+        {
+            if (mailNotificationEntity == null && newWhenIsNull)
+                return new MailNotificationDataModel
+                {
+                    Id = -1,
+                };
+
+            if (mailNotificationEntity == null && !newWhenIsNull)
+                throw new ArgumentNullException("mailNotificationEntity", "Cannot create MailNotificationDataModel from NULL mailNotification entity.");
+
+            var notificationPlugin = service.GetNotificationPlugin(mailNotificationEntity.NotificationPluginSystemName);
+
+            var notificationDataModel = new MailNotificationDataModel
+            {
+                Id = mailNotificationEntity.Id,
+                CronExpression = mailNotificationEntity.CronExpression,
+                NotificationPlugin = notificationPlugin,
+                NotificationPluginSystemName = mailNotificationEntity.NotificationPluginSystemName,
+                NotificationJobSystemName = mailNotificationEntity.NotificationJobSystemName,
+                UserValues =
+                    mailNotificationEntity.Users
+                        .Select(r => r.Id)
+                        .ToList(),
+                UserDescriptions =
+                    mailNotificationEntity.Users
+                        .Select(r => String.Format("{0} - ({1})", r.Lastname,
+                            String.Join(", ", r.Roles.Select(u => u.Description))))
+                        .ToList(),
+                Users =
+                    mailNotificationEntity.Users
+                        .Select(r => UserDataModel.FromUser(r, false))
+                        .ToList(),
+            };
+
+            notificationDataModel.NotificationPluginSystemNameDescription =
+                    notificationPlugin == null
+                        ? String.Empty
+                        : notificationPlugin.Name;
+
+            notificationDataModel.NotificationJobSystemNameDescription =
+                    notificationPlugin == null
+                        ? String.Empty
+                        : notificationPlugin.AvailableNotificationJobs
+                            .Any(j => j.SystemName == notificationDataModel.NotificationJobSystemName)
+                                ? notificationPlugin.AvailableNotificationJobs
+                                    .First(j => j.SystemName == notificationDataModel.NotificationJobSystemName).Name
+                                : String.Empty;
+
+            return notificationDataModel;
+        }
+
+        public Core.Domain.Misc.MailNotification ToMailNotification()
+        {
+            return new Core.Domain.Misc.MailNotification
+            {
+                Id = this.Id,
+                CronExpression = this.CronExpression,
+                NotificationPluginSystemName = this.NotificationPluginSystemName,
+                NotificationJobSystemName = this.NotificationJobSystemName
+            };
+        }
+    }
+}

+ 149 - 37
GreenTree.Nachtragsmanagement.Web/Scheduling/AppendixNotificationPlugin.cs

@@ -10,6 +10,7 @@ using GreenTree.Nachtragsmanagement.Services.Appendix;
 using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
 using System.Net.Mail;
 using System.Net;
+using System.Globalization;
 
 namespace GreenTree.Nachtragsmanagement.Web.Scheduling
 {
@@ -136,61 +137,109 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
                 {
                     case "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate":
                         {
-                            var conversionSuccess = false;
-
-                            var ageDaysConfigItem = _configurationService.GetConfigItemByName(
-                                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.AgeDays");
+                            ProcessNegotiationDateNotification(notification);
+                        }
+                        break;
+                    case "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol":
+                        {
+                            ProcessNegotiationProtocolNotification(notification);
+                        }
+                        break;
+                    default:
+                        continue;
+                }
+            }
+        }
 
-                            var ageDays = ageDaysConfigItem == null
-                                ? 56
-                                : _configurationService.TryGetConfigItemValue<int>(ageDaysConfigItem, out conversionSuccess);
+        /// <summary>
+        /// Sets the corresponding status for appendices which offering date is older than N weeks and notifies 
+        /// the correspondig recipients
+        /// </summary>
+        /// <param name="mailNotification">The notification which shall be generated.</param>
+        private void ProcessNegotiationDateNotification(MailNotification mailNotification)
+        {
+            var ageDays = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.AgeDays", 56);
 
-                            if (!conversionSuccess)
-                                ageDays = 56;
+            var stateConditionId = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.StateCondition", 1);
 
-                            var stateSetConfigItem = _configurationService.GetConfigItemByName(
-                                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.StateSet");
+            var stateSetId = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.StateSet", 2);
 
-                            var stateId = stateSetConfigItem == null
-                                ? 2
-                                : _configurationService.TryGetConfigItemValue<int>(stateSetConfigItem, out conversionSuccess);
+            var interval = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationDate.Interval", 2);
 
-                            if (!conversionSuccess)
-                                stateId = 2;
+            interval = interval == 0
+                ? 1
+                : interval;
 
-                            var appendices = _appendixService.GetAllAppendices()
-                                .Where(a => a.OfferingDate <= DateTime.Now.AddDays(ageDays) &&
-                                            a.StateId != stateId &&
-                                            a.NegotiationDate == null)
-                                .ToList();
+            var appendices = _appendixService.GetAllAppendices()
+                .Where(a => a.OfferingDate.HasValue &&
+                            a.OfferingDate <= DateTime.Now.AddDays(ageDays) &&
+                            a.StateId == stateConditionId &&
+                            a.NegotiationDate == null)
+                .ToList();
 
-                            var mailBody = GenerateNegotiationDateMailBody(appendices);
+            var currentCalendarWeek = GetCalendarWeek(DateTime.Now);
 
-                            foreach (var appendix in appendices)
-                            {
-                                appendix.StateId = stateId;
+            appendices = appendices
+                .Where(a => ((currentCalendarWeek - GetCalendarWeek(a.OfferingDate.Value) % interval == 0)))
+                .ToList();
 
-                                _appendixService.UpdateAppendix(appendix);
-                            }
+            if (appendices.Any())
+            {
+                var mailBody = GenerateNegotiationDateMailBody(appendices);
 
-                            SendNotification(notification, "Autom. Übersicht nicht verhandelte Nachträge", mailBody);
-                        }
-                        break;
-                    case "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol":
+                foreach (var appendix in appendices)
+                {
+                    appendix.StateId = stateSetId;
 
-                        break;
-                    default:
-                        continue;
+                    _appendixService.UpdateAppendix(appendix);
                 }
+
+                SendNotification(mailNotification, "Autom. Übersicht nicht verhandelte Nachträge", mailBody);
             }
         }
 
         /// <summary>
-        /// Sets the corresponding status for appendices which offering date is older than 8 weeks and notifies the correspondig recipients
+        /// Sets the corresponding status for appendices which negotiation date is older than N weeks and notifies 
+        /// the correspondig recipients
         /// </summary>
-        private void ProcessNegotiationDateNotification()
+        /// <param name="mailNotification">The notification which shall be generated.</param>
+        private void ProcessNegotiationProtocolNotification(MailNotification mailNotification)
         {
+            var ageDays = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.AgeDays", 14);
+
+            var stateConditionId = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.StateCondition", 3);
+
+            var interval = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol.Interval", 2);
+
+            interval = interval == 0 
+                ? 1 
+                : interval;
 
+            var appendices = _appendixService.GetAllAppendices()
+                .Where(a => a.NegotiationDate.HasValue &&
+                            a.NegotiationDate <= DateTime.Now.AddDays(ageDays) &&
+                            a.StateId == stateConditionId)
+                .ToList();
+
+            var currentCalendarWeek = GetCalendarWeek(DateTime.Now);
+
+            appendices = appendices
+                .Where(a => ((currentCalendarWeek - GetCalendarWeek(a.OfferingDate.Value) % interval == 0)))
+                .ToList();
+
+            if (appendices.Any())
+            {
+                var mailBody = GenerateNegotiationProtocolMailBody(appendices);
+
+                SendNotification(mailNotification, "Autom. Übersicht verhandelte Nachträge o. Protokoll", mailBody);
+            }
         }
 
         #endregion
@@ -198,7 +247,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
         #region Mail body generation
 
         /// <summary>
-        /// Generates a mail body with a list of all appendices with a offering date later than 8 weeks
+        /// Generates a mail body with a list of all appendices with a offering date later than N weeks
         /// </summary>
         /// <param name="appendices">Appendices matching that criteria.</param>
         public string GenerateNegotiationDateMailBody(IEnumerable<Appendix> appendices)
@@ -229,6 +278,37 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
             return String.Format(template, appendicesList);
         }
 
+        /// <summary>
+        /// Generates a mail body with a list of all appendices with a negotiation date later than N weeks
+        /// </summary>
+        /// <param name="appendices">Appendices matching that criteria.</param>
+        public string GenerateNegotiationProtocolMailBody(IEnumerable<Appendix> appendices)
+        {
+            var template =
+                "<html>" +
+                "   <body>" +
+                "       <h3>Übersicht \"Verhandelte Nachträge ohne Protokoll\"</h3>" +
+                "       <p>Folgende Nachträge haben ein Verhandlungstermin älter als zwei Wochen, jedoch noch kein Protokoll:" +
+                "       <ul>" +
+                "           {0}" +
+                "       </ul>" +
+                "   </body>" +
+                "</html>";
+
+            if (!appendices.Any()) return String.Format(template, String.Empty);
+
+            var appendicesList = String.Empty;
+
+            foreach (var appendix in appendices)
+            {
+                appendicesList += String.Format(
+                    "<li>Nachtrag <b>\"{0}\"</b> in Baustelle <b>\"{1}\"</b> - Verhandlungsdatum: <b>{2:dd.MM.yyyy}</b>",
+                    appendix.CustomNumber, appendix.Site.CustomNumber, appendix.NegotiationDate);
+            }
+
+            return String.Format(template, appendicesList);
+        }
+
         #endregion
 
         #region Mail sending
@@ -273,5 +353,37 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
         }
 
         #endregion
+
+        #region Helper
+
+        /// <summary>
+        /// Determines the calendar week of a specific datetime
+        /// </summary>
+        /// <param name="date">The datetime which calendarweek should be calculated.</param>
+        public static int GetCalendarWeek(DateTime date)
+        {
+            var currentCulture = CultureInfo.CurrentCulture;
+            var calendar = currentCulture.Calendar;
+
+            var calendarWeek = calendar.GetWeekOfYear(
+                date, 
+                currentCulture.DateTimeFormat.CalendarWeekRule, 
+                currentCulture.DateTimeFormat.FirstDayOfWeek);
+
+            if (calendarWeek > 52)
+            {
+                date = date.AddDays(7);
+                var testCalendarWeek = calendar.GetWeekOfYear(date,
+                   currentCulture.DateTimeFormat.CalendarWeekRule,
+                   currentCulture.DateTimeFormat.FirstDayOfWeek);
+
+                if (testCalendarWeek == 2)
+                    calendarWeek = 1;
+            }
+
+            return calendarWeek;
+        }
+
+        #endregion
     }
 }

+ 114 - 0
GreenTree.Nachtragsmanagement.Web/Views/Misc/MailNotifications.cshtml

@@ -0,0 +1,114 @@
+@{
+	Layout = "~/Views/Shared/_FunctionLayout.cshtml";
+}
+
+@model IEnumerable<GreenTree.Nachtragsmanagement.Web.Models.Misc.MailNotificationDataModel>
+
+<script>
+	var deleteId;
+	var gridScrollHeight;
+	var gridScrollOffset = 280;
+	var resizeFinished;
+
+	$(document).ready(function () {
+		gridScrollHeight = $(window).height() - gridScrollOffset;
+		setTimeout(function () {
+			devGridViewMailNotifications.PerformCallback();
+		}, 500);
+	});
+
+	$(window).resize(function () {
+		clearTimeout(window.resizedFinished);
+		window.resizedFinished = setTimeout(function () {
+			gridScrollHeight = $(window).height() - gridScrollOffset;
+			devGridViewMailNotifications.PerformCallback();
+		}, 250);
+	});
+
+	function onToolbarItemClick(s, e) {
+		if (!s || !e) return;
+		if (IsExportToolbarCommand(e.item.name)) {
+			$("#Format").val(e.item.name);
+			$("#mailNotificationExportForm").submit();
+		} else if (e.item.name == "ToggleColumnChooser") {
+			if (devGridViewMailNotifications.IsCustomizationWindowVisible())
+				devGridViewMailNotifications.HideCustomizationWindow();
+			else
+				devGridViewMailNotifications.ShowCustomizationWindow();
+		}
+	}
+
+	function IsExportToolbarCommand(command) {
+		return command == "Pdf" || command == "Xlsx" || command == "Xls";
+	}
+
+	function editMailNotification(id) {
+		if (!id) return;
+		$.ajax({
+			url: '@Url.Action("EditMailNotification", "Misc")',
+			data: { Id: id },
+			success: function (response) {
+				setTimeout(function () {
+					$(".mailNotificationEditContainer").remove();
+					$("body").append(response);
+				}, 200);
+			},
+			error: function () {
+				 alert("error occured");
+			}
+		});
+	}
+
+	function confirmDelete(id) {
+		if (!id) return;
+		deleteId = id;
+		$.ajax({
+			type: "GET",
+			url: '@Url.Action("GetMailNotification", "Misc")',
+			data: { Id: id },
+			success: function (response) {
+				if (response == "notFound") return;
+				var mailNotification = JSON.parse(response);
+				var popupControl = MVCxClientPopupControl.Cast(devPopupControlDeleteMailNotification);
+				popupControl.SetHeaderText(popupControl.GetHeaderText().replace("{mailNotification}", mailNotification.CustomNumber));
+				$(".dialogTextMailNotification").text($(".dialogTextMailNotification").text().replace("{mailNotification}", mailNotification.CustomNumber));
+				popupControl.Show();
+			}
+		});
+	}
+
+	function deleteMailNotification() {
+		$.ajax({
+			type: "POST",
+			url: '@Url.Action("DeleteMailNotification", "Misc")',
+			data: { Id: deleteId },
+			success: function (response) {
+				var popupControl = MVCxClientPopupControl.Cast(devPopupControlDeleteMailNotification);
+				popupControl.Hide();
+				setTimeout(function () {
+					devGridViewMailNotifications.PerformCallback();
+				}, 200);
+			},
+			error: function () {
+				 alert("error occured");
+			}
+		});
+	}
+</script>
+
+@using (Html.BeginForm("ExportPartialMailNotifications", "Misc", FormMethod.Post, new { id = "mailNotificationExportForm" }))
+{
+	@Html.Hidden("Format")
+}
+
+@Html.Partial("~/Views/Misc/_MailNotificationGridPartial.cshtml", Model)
+@Html.Partial("~/Views/Shared/_PopupDialogYesNo.cshtml", new GreenTree.Nachtragsmanagement.Web.Models.Global.YesNoDialogModel
+{
+	PopupName = "devPopupControlDeleteMailNotification",
+	Content = "<div class='dialogTextMailNotification' style='padding: 12px'>Sind Sie sicher, dass Sie die Benachrichtigung " +
+			  "\"{mailNotification}\"?</div>",
+	HeaderText = "\"{mailNotification}\" löschen",
+	YesFunction = "function (s, e) { deleteMailNotification(); }",
+	YesButtonName = "devButtonDeleteMailNotificationYes",
+	NoButtonName = "devButtonDeleteMailNotificationNo"
+})

+ 193 - 0
GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationEditPartial.cshtml

@@ -0,0 +1,193 @@
+@using GreenTree.Nachtragsmanagement.Web.Extensions
+
+@{ 
+	var userContext = GreenTree.Nachtragsmanagement.Core.CommonHelper.UserContext();
+}
+
+@model GreenTree.Nachtragsmanagement.Web.Models.Misc.MailNotificationDataModel
+
+<div class="mailNotificationEditContainer">
+
+	<script>
+		var textSeparator = ", ";
+		var deleteId = null;
+		var pluginSystemName = "";
+
+		function saveMailNotification() {
+			var form = $("#mailNotificationEditForm");
+			$(form).submit(function (e) {
+				$.ajax({
+					type: "POST",
+					url: '@Url.Action("EditMailNotification", "Misc")',
+					data: form.serialize(),
+					success: function (response) {
+						setTimeout(function () {
+							$(".mailNotificationEditContainer").remove();
+							if (response == "success") {
+								devGridViewMailNotification.PerformCallback();
+							} else {
+								$("body").append(response);
+							}
+						}, 200);
+					}
+				});
+				e.preventDefault();
+			});
+			form.submit();
+		}
+
+		function onListBoxSelectionChanged(s, e) {
+			updateText();
+		}
+
+		function updateText() {
+			var selectedItems = UserValues.GetSelectedItems();
+			devDropDownListUserValues.SetText(getSelectedItemsText(selectedItems));
+		}
+
+		function synchronizeListBoxValues(s, e) {
+			UserValues.UnselectAll();
+			var texts = s.GetText().split(textSeparator);
+			var values = getValuesByTexts(texts);
+			UserValues.SelectValues(values);
+			updateText();
+		}
+
+		function getSelectedItemsText(items) {
+			var texts = [];
+			for (var i = 0; i < items.length; i++)
+				texts.push(items[i].text);
+			return texts.join(textSeparator);
+		}
+
+		function getValuesByTexts(texts) {
+			var actualValues = [];
+			var item;
+			for (var i = 0; i < texts.length; i++) {
+				item = UserValues.FindItemByText(texts[i]);
+				if (item != null)
+					actualValues.push(item.value);
+			}
+			return actualValues;
+		}
+	</script>
+
+	@Html.DevExpress().PopupControl(s =>
+{
+	s.Name = "devPopupControlEditMailNotification";
+
+	if (Model.Id == -1)
+		s.HeaderText = "Neue Benachrichtigung erstellen";
+	else
+		s.HeaderText = "\"" + Model.NotificationPluginSystemNameDescription + " - " + Model.NotificationJobSystemNameDescription + "\" bearbeiten";
+
+	s.Modal = true;
+	s.Width = new Unit(600, UnitType.Pixel);
+	s.CloseAction = CloseAction.CloseButton;
+	s.PopupHorizontalAlign = PopupHorizontalAlign.WindowCenter;
+	s.PopupVerticalAlign = PopupVerticalAlign.TopSides;
+	s.PopupVerticalOffset = 10;
+	s.AllowDragging = true;
+	s.AllowResize = false;
+	s.ShowMaximizeButton = true;
+	s.ShowFooter = false;
+	s.ShowOnPageLoad = true;
+	s.SetContent(() =>
+	{
+		using (Html.BeginForm("EditMailNotification", "Misc", FormMethod.Post, new { id = "mailNotificationEditForm" }))
+		{
+			ViewContext.Writer.Write("<div class='editFormWrapper'>");
+
+			ViewContext.Writer.Write("<input type=\"hidden\" value=\"" + Model.Id + "\" id=\"Id\" name=\"Id\" />");
+
+			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
+			{
+				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 50%'>");
+				{
+					ViewContext.Writer.Write(Html.CustomLabelFor(m => m.NotificationPluginSystemName, "Benachrichtigungs-Plugin:"));
+					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.NotificationPluginSystemName).ToHtmlString());
+					Html.DevExpress().ComboBoxFor(m => m.NotificationPluginSystemName, t =>
+					{
+						t.Width = new Unit(95, UnitType.Percentage);
+						t.Properties.ValueField = "SystemName";
+						t.Properties.ValueType = typeof(string);
+						t.Properties.TextField = "Name";
+						t.Properties.ClientSideEvents.SelectedIndexChanged = "function (s, e) { pluginSystemName = s.GetValue(); }";
+					}).BindList(ViewData["AllNotificationPlugins"]).Bind(Model.NotificationPluginSystemName).Render();
+				}
+				ViewContext.Writer.Write("</div>");
+
+				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 50%'>");
+				{
+					ViewContext.Writer.Write(Html.CustomLabelFor(m => m.NotificationJobSystemName, "Benachrichtigungs-Job:"));
+					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.NotificationJobSystemName).ToHtmlString());
+					ViewContext.Writer.Write(Html.Partial("~/Views/Misc/_MailNotificationPluginJobsPartial.cshtml", Model).ToHtmlString());
+				}
+				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
+
+			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
+			{
+				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 100%'>");
+				{
+					ViewContext.Writer.Write(Html.CustomLabelFor(m => m.UserValues, "Benutzer:"));
+					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.UserValues).ToHtmlString());
+					Html.DevExpress().DropDownEdit(t =>
+					{
+						t.Name = "devDropDownListUserValues";
+						t.Width = new Unit(100, UnitType.Percentage);
+						t.Text = Model.UserDescription;
+
+						t.SetDropDownWindowTemplateContent(l =>
+						{
+							Html.DevExpress().ListBox(lb =>
+							{
+								lb.Name = "UserValues";
+								lb.Width = new Unit(100, UnitType.Percentage);
+								lb.Height = new Unit(250, UnitType.Pixel);
+								lb.Properties.TextField = "Description";
+								lb.Properties.ValueField = "Id";
+								lb.Properties.ValueType = typeof(int);
+								lb.Properties.SelectionMode = ListEditSelectionMode.CheckColumn;
+								lb.ControlStyle.Border.BorderStyle = BorderStyle.None;
+								lb.PreRender = (sender, e) =>
+								{
+									var listBox = sender as MVCxListBox;
+
+									foreach (ListEditItem listItem in listBox.Items)
+									{
+										if (Model.UserValues == null || !Model.UserValues.Any(m => m == (int)listItem.Value)) continue;
+
+										listItem.Selected = true;
+									}
+								};
+								lb.Properties.ClientSideEvents.SelectedIndexChanged = "function (s, e) { onListBoxSelectionChanged(s, e); }";
+							}).BindList(ViewData["AllUsersWithRole"]).Render();
+
+							t.Properties.ClientSideEvents.TextChanged = "function (s, e) { synchronizeListBoxValues(s, e); }";
+							t.Properties.ClientSideEvents.DropDown = "function (s, e) { synchronizeListBoxValues(s, e); }";
+						});
+					}).Render();
+				}
+				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
+
+			ViewContext.Writer.Write("</div>");
+
+			Html.RenderPartial(
+				"~/Views/Shared/_PopupButtonPanel.cshtml",
+				new GreenTree.Nachtragsmanagement.Web.Models.Global.PopupModel
+				{
+					PopupName = "devPopupControlEditMailNotification",
+					AcceptFunction = "function (s, e) { saveMailNotification(); }"
+				}
+			);
+		}
+	});
+	s.Styles.Content.Paddings.Padding = new Unit(0);
+	s.Styles.ModalBackground.Opacity = 0;
+}).GetHtml()
+
+</div>

+ 9 - 0
GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationGridPartial.cshtml

@@ -0,0 +1,9 @@
+@model IEnumerable<GreenTree.Nachtragsmanagement.Web.Models.Misc.MailNotificationDataModel>
+
+@using GreenTree.Nachtragsmanagement.Web.Extensions
+
+@{ 
+	var userContext = GreenTree.Nachtragsmanagement.Core.CommonHelper.UserContext();
+}
+
+@Html.DevExpress().GridView(Html.MailNotificationGridViewSettings()).Bind(Model).GetHtml()

+ 26 - 0
GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationPluginJobsPartial.cshtml

@@ -0,0 +1,26 @@
+@model GreenTree.Nachtragsmanagement.Web.Models.Misc.MailNotificationDataModel
+
+@if (Model == null)
+{
+	@Html.DevExpress().ComboBoxFor(m => m.NotificationJobSystemName, t =>
+	{
+		t.Width = new Unit(100, UnitType.Percentage);
+		t.Properties.ValueField = "SystemName";
+		t.Properties.ValueType = typeof(string);
+		t.Properties.TextField = "Name";
+		t.CallbackRouteValues = new { Controller = "Misc", View = "PartialNotificationPluginJobs" };
+		t.Properties.ClientSideEvents.BeginCallback = "function (s, e) { e.customArgs['pluginSystemName'] = [ pluginSystemName ]; }";
+	}).GetHtml()
+}
+else
+{
+	@Html.DevExpress().ComboBoxFor(m => m.NotificationJobSystemName, t =>
+	{
+		t.Width = new Unit(100, UnitType.Percentage);
+		t.Properties.ValueField = "SystemName";
+		t.Properties.ValueType = typeof(string);
+		t.Properties.TextField = "Name";
+		t.CallbackRouteValues = new { Controller = "Misc", View = "PartialNotificationPluginJobs" };
+		t.Properties.ClientSideEvents.BeginCallback = "function (s, e) { e.customArgs['pluginSystemName'] = [ pluginSystemName ]; }";
+	}).BindList(Model.NotificationPlugin.AvailableNotificationJobs).Bind(Model.NotificationJobSystemName).GetHtml()
+}