瀏覽代碼

Benachrichtigung zu 99% fertiggestellt.

Arne Diekmann 8 年之前
父節點
當前提交
3033edf3cb
共有 24 個文件被更改,包括 1255 次插入59 次删除
  1. 8 0
      GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj
  2. 3 0
      GreenTree.Nachtragsmanagement.Services/Misc/NotificationService.cs
  3. 28 0
      GreenTree.Nachtragsmanagement.Services/Scheduling/INotificationScheduler.cs
  4. 119 0
      GreenTree.Nachtragsmanagement.Services/Scheduling/NotificationScheduler.cs
  5. 364 0
      GreenTree.Nachtragsmanagement.Services/job_scheduling_data_2_0.xsd
  6. 1 0
      GreenTree.Nachtragsmanagement.Services/packages.config
  7. 2 0
      GreenTree.Nachtragsmanagement.Web.Framework/ApplicationContext.cs
  8. 4 1
      GreenTree.Nachtragsmanagement.Web/Controllers/GlobalController.cs
  9. 47 6
      GreenTree.Nachtragsmanagement.Web/Controllers/MiscController.cs
  10. 11 5
      GreenTree.Nachtragsmanagement.Web/Extensions/GridViewSettingsHelper.cs
  11. 6 0
      GreenTree.Nachtragsmanagement.Web/Global.asax.cs
  12. 13 3
      GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj
  13. 26 5
      GreenTree.Nachtragsmanagement.Web/Implementations/AppendixNotificationPlugin.cs
  14. 313 0
      GreenTree.Nachtragsmanagement.Web/Implementations/DeviationNotificationPlugin.cs
  15. 11 1
      GreenTree.Nachtragsmanagement.Web/Models/Misc/MailNotificationDataModel.cs
  16. 0 12
      GreenTree.Nachtragsmanagement.Web/Scheduling/JobScheduler.cs
  17. 0 12
      GreenTree.Nachtragsmanagement.Web/Scheduling/JobWorker.cs
  18. 0 0
      GreenTree.Nachtragsmanagement.Web/Scripts/jquery-cron-min.js
  19. 3 0
      GreenTree.Nachtragsmanagement.Web/Validation/AppendixValidatorFactory.cs
  20. 32 0
      GreenTree.Nachtragsmanagement.Web/Validation/Misc/MailNotificationDataModelValidator.cs
  21. 48 3
      GreenTree.Nachtragsmanagement.Web/Views/Misc/MailNotifications.cshtml
  22. 178 8
      GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationEditPartial.cshtml
  23. 5 3
      GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationPluginJobsPartial.cshtml
  24. 33 0
      GreenTree.Nachtragsmanagement.Web/packages.config

+ 8 - 0
GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj

@@ -43,6 +43,9 @@
     <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
       <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
     </Reference>
+    <Reference Include="Quartz, Version=2.6.0.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4, processorArchitecture=MSIL">
+      <HintPath>..\packages\Quartz.2.6.0\lib\net40\Quartz.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.configuration" />
     <Reference Include="System.Core" />
@@ -68,6 +71,8 @@
     <Compile Include="Misc\IMiscService.cs" />
     <Compile Include="Misc\NotificationService.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Scheduling\INotificationScheduler.cs" />
+    <Compile Include="Scheduling\NotificationScheduler.cs" />
     <Compile Include="Site\ISiteService.cs" />
     <Compile Include="Site\SiteService.cs" />
     <Compile Include="Test\DbRelationFormat.cs" />
@@ -90,6 +95,9 @@
   </ItemGroup>
   <ItemGroup>
     <None Include="app.config" />
+    <None Include="job_scheduling_data_2_0.xsd">
+      <SubType>Designer</SubType>
+    </None>
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup />

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

@@ -49,6 +49,9 @@ namespace GreenTree.Nachtragsmanagement.Services.Misc
         /// <param name="pluginSystemName">SystemName of notification plugin.</param>
         public INotificationPlugin GetNotificationPlugin(string pluginSystemName)
         {
+            if (String.IsNullOrEmpty(pluginSystemName))
+                return null;
+
             var notificationPlugin = Singleton<IContainer>.Instance.ResolveNamed<INotificationPlugin>(pluginSystemName);
 
             if (notificationPlugin != null)

+ 28 - 0
GreenTree.Nachtragsmanagement.Services/Scheduling/INotificationScheduler.cs

@@ -0,0 +1,28 @@
+using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Services.Scheduling
+{
+    public interface INotificationScheduler
+    {
+        /// <summary>
+        /// Starts the scheduler and builds all corresponding notification jobs
+        /// </summary>
+        void Start();
+
+        /// <summary>
+        /// Determines the next execution time of a specific job
+        /// </summary>
+        /// <param name="jobId">The job id.</param>
+        DateTime GetNextExecutionOfJob(string jobId);
+
+        /// <summary>
+        /// Determines the next execution time of a specific mail notification
+        /// </summary>
+        /// <param name="mailNotification">The mail notification job.</param>
+        DateTime GetNextExecutionOfJob(MailNotification mailNotification);
+    }
+}

+ 119 - 0
GreenTree.Nachtragsmanagement.Services/Scheduling/NotificationScheduler.cs

@@ -0,0 +1,119 @@
+using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
+using GreenTree.Nachtragsmanagement.Services.Misc;
+using Quartz;
+using Quartz.Impl;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Services.Scheduling
+{
+    public class NotificationScheduler : INotificationScheduler
+    {
+        #region Fields
+
+        private readonly INotificationService _notificationService;
+        private readonly IMiscService _miscService;
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the NotificationScheduler class
+        /// </summary>
+        public NotificationScheduler(
+            INotificationService notificationService,
+            IMiscService miscService)
+        {
+            _notificationService = notificationService;
+            _miscService = miscService;
+        }
+
+        #endregion
+
+        /// <summary>
+        /// Starts the scheduler and builds all corresponding notification jobs
+        /// </summary>
+        public void Start()
+        {
+            var scheduler = StdSchedulerFactory.GetDefaultScheduler();
+
+            scheduler.Clear();
+            scheduler.Start();
+
+            var mailNotifications = _miscService.GetAllMailNotifications();
+
+            foreach (var mailNotification in mailNotifications)
+            {
+                var notificationPlugin = _notificationService.GetNotificationPlugin(mailNotification.NotificationPluginSystemName);
+
+                if (notificationPlugin == null)
+                    continue;
+
+                try
+                {
+                    var job = JobBuilder.Create(notificationPlugin.GetType())
+                        .WithIdentity(mailNotification.Id.ToString())
+                        .SetJobData(new JobDataMap
+                        {
+                                { "MailNotifications", new [] { mailNotification } }
+                        })
+                        .Build();
+
+                    var trigger = TriggerBuilder.Create()
+                        .WithCronSchedule(mailNotification.CronExpression)
+                        .Build();
+
+                    scheduler.ScheduleJob(job, trigger);
+                }
+                catch (Exception ex)
+                {
+                    continue;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Determines the next execution time of a specific job
+        /// </summary>
+        /// <param name="jobId">The job id.</param>
+        public DateTime GetNextExecutionOfJob(string jobId)
+        {
+            var scheduler = StdSchedulerFactory.GetDefaultScheduler();
+
+            var jobKey = new JobKey(jobId);
+            var nextFireTime = DateTime.MinValue;
+
+            var isJobExisting = scheduler.CheckExists(jobKey);
+
+            if (isJobExisting)
+            {
+                var detail = scheduler.GetJobDetail(jobKey);
+                var triggers = scheduler.GetTriggersOfJob(jobKey);
+
+                if (triggers.Count > 0)
+                {
+                    var nextFireTimeUtc = triggers[0].GetNextFireTimeUtc();
+
+                    nextFireTime = TimeZone.CurrentTimeZone.ToLocalTime(nextFireTimeUtc.Value.DateTime);
+                }
+            }
+
+            return (nextFireTime);
+        }
+
+        /// <summary>
+        /// Determines the next execution time of a specific mail notification
+        /// </summary>
+        /// <param name="mailNotification">The mail notification job.</param>
+        public DateTime GetNextExecutionOfJob(MailNotification mailNotification)
+        {
+            if (mailNotification == null)
+                return DateTime.MinValue;
+
+            return GetNextExecutionOfJob(mailNotification.Id.ToString());
+        }
+    }
+}

+ 364 - 0
GreenTree.Nachtragsmanagement.Services/job_scheduling_data_2_0.xsd

@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
+           targetNamespace="http://quartznet.sourceforge.net/JobSchedulingData"
+           elementFormDefault="qualified"
+           version="2.0">
+
+  <xs:element name="job-scheduling-data">
+    <xs:annotation>
+      <xs:documentation>Root level node</xs:documentation>
+    </xs:annotation>
+    <xs:complexType>
+      <xs:sequence maxOccurs="unbounded">
+        <xs:element name="pre-processing-commands" type="pre-processing-commandsType" minOccurs="0" maxOccurs="1">
+          <xs:annotation>
+            <xs:documentation>Commands to be executed before scheduling the jobs and triggers in this file.</xs:documentation>
+          </xs:annotation>
+        </xs:element>
+        <xs:element name="processing-directives" type="processing-directivesType" minOccurs="0" maxOccurs="1">
+          <xs:annotation>
+            <xs:documentation>Directives to be followed while scheduling the jobs and triggers in this file.</xs:documentation>
+          </xs:annotation>
+        </xs:element>
+        <xs:element name="schedule" minOccurs="0" maxOccurs="unbounded">
+          <xs:complexType>
+            <xs:sequence maxOccurs="unbounded">
+              <xs:element name="job" type="job-detailType" minOccurs="0" maxOccurs="unbounded" />
+              <xs:element name="trigger" type="triggerType" minOccurs="0" maxOccurs="unbounded" />
+            </xs:sequence>
+          </xs:complexType>
+        </xs:element>
+      </xs:sequence>
+      <xs:attribute name="version" type="xs:string">
+        <xs:annotation>
+          <xs:documentation>Version of the XML Schema instance</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+    </xs:complexType>
+  </xs:element>
+
+  <xs:complexType name="pre-processing-commandsType">
+    <xs:sequence maxOccurs="unbounded">
+      <xs:element name="delete-jobs-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
+        <xs:annotation>
+          <xs:documentation>Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs.</xs:documentation>
+        </xs:annotation>
+      </xs:element>
+      <xs:element name="delete-triggers-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
+        <xs:annotation>
+          <xs:documentation>Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable.</xs:documentation>
+        </xs:annotation>
+      </xs:element>
+      <xs:element name="delete-job" minOccurs="0" maxOccurs="unbounded">
+        <xs:annotation>
+          <xs:documentation>Delete the identified job if it exists (will also result in deleting all triggers related to it).</xs:documentation>
+        </xs:annotation>
+        <xs:complexType>
+          <xs:sequence>
+            <xs:element name="name" type="xs:string" />
+            <xs:element name="group" type="xs:string" minOccurs="0" />
+          </xs:sequence>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="delete-trigger" minOccurs="0" maxOccurs="unbounded">
+        <xs:annotation>
+          <xs:documentation>Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable).</xs:documentation>
+        </xs:annotation>
+        <xs:complexType>
+          <xs:sequence>
+            <xs:element name="name" type="xs:string" />
+            <xs:element name="group" type="xs:string" minOccurs="0" />
+          </xs:sequence>
+        </xs:complexType>
+      </xs:element>
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="processing-directivesType">
+    <xs:sequence>
+      <xs:element name="overwrite-existing-data" type="xs:boolean" minOccurs="0" default="true">
+        <xs:annotation>
+          <xs:documentation>Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur.</xs:documentation>
+        </xs:annotation>
+      </xs:element>
+      <xs:element name="ignore-duplicates" type="xs:boolean" minOccurs="0" default="false">
+        <xs:annotation>
+          <xs:documentation>If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced.</xs:documentation>
+        </xs:annotation>
+      </xs:element>
+      <xs:element name="schedule-trigger-relative-to-replaced-trigger" type="xs:boolean" minOccurs="0" default="false">
+        <xs:annotation>
+          <xs:documentation>If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work.</xs:documentation>
+        </xs:annotation>
+      </xs:element>
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="job-detailType">
+    <xs:annotation>
+      <xs:documentation>Define a JobDetail</xs:documentation>
+    </xs:annotation>
+    <xs:sequence>
+      <xs:element name="name" type="xs:string" />
+      <xs:element name="group" type="xs:string" minOccurs="0" />
+      <xs:element name="description" type="xs:string" minOccurs="0" />
+      <xs:element name="job-type" type="xs:string" />
+      <xs:sequence minOccurs="0">
+        <xs:element name="durable" type="xs:boolean" />
+        <xs:element name="recover" type="xs:boolean" />
+      </xs:sequence>
+      <xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="job-data-mapType">
+    <xs:annotation>
+      <xs:documentation>Define a JobDataMap</xs:documentation>
+    </xs:annotation>
+    <xs:sequence minOccurs="0" maxOccurs="unbounded">
+      <xs:element name="entry" type="entryType" />
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="entryType">
+    <xs:annotation>
+      <xs:documentation>Define a JobDataMap entry</xs:documentation>
+    </xs:annotation>
+    <xs:sequence>
+      <xs:element name="key" type="xs:string" />
+      <xs:element name="value" type="xs:string" />
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="triggerType">
+    <xs:annotation>
+      <xs:documentation>Define a Trigger</xs:documentation>
+    </xs:annotation>
+    <xs:choice>
+      <xs:element name="simple" type="simpleTriggerType" />
+      <xs:element name="cron" type="cronTriggerType" />
+      <xs:element name="calendar-interval" type="calendarIntervalTriggerType" />
+    </xs:choice>
+  </xs:complexType>
+
+  <xs:complexType name="abstractTriggerType" abstract="true">
+    <xs:annotation>
+      <xs:documentation>Common Trigger definitions</xs:documentation>
+    </xs:annotation>
+    <xs:sequence>
+      <xs:element name="name" type="xs:string" />
+      <xs:element name="group" type="xs:string" minOccurs="0" />
+      <xs:element name="description" type="xs:string" minOccurs="0" />
+      <xs:element name="job-name" type="xs:string" />
+      <xs:element name="job-group" type="xs:string" minOccurs="0" />
+      <xs:element name="priority" type="xs:nonNegativeInteger" minOccurs="0" />
+      <xs:element name="calendar-name" type="xs:string" minOccurs="0" />
+      <xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
+      <xs:sequence minOccurs="0">
+        <xs:choice>
+          <xs:element name="start-time" type="xs:dateTime" />
+          <xs:element name="start-time-seconds-in-future" type="xs:nonNegativeInteger" />
+        </xs:choice>
+        <xs:element name="end-time" type="xs:dateTime" minOccurs="0" />
+      </xs:sequence>
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="simpleTriggerType">
+    <xs:annotation>
+      <xs:documentation>Define a SimpleTrigger</xs:documentation>
+    </xs:annotation>
+    <xs:complexContent>
+      <xs:extension base="abstractTriggerType">
+        <xs:sequence>
+          <xs:element name="misfire-instruction" type="simple-trigger-misfire-instructionType" minOccurs="0" />
+          <xs:sequence minOccurs="0">
+            <xs:element name="repeat-count" type="repeat-countType" />
+            <xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
+          </xs:sequence>
+        </xs:sequence>
+      </xs:extension>
+    </xs:complexContent>
+  </xs:complexType>
+
+  <xs:complexType name="cronTriggerType">
+    <xs:annotation>
+      <xs:documentation>Define a CronTrigger</xs:documentation>
+    </xs:annotation>
+    <xs:complexContent>
+      <xs:extension base="abstractTriggerType">
+        <xs:sequence>
+          <xs:element name="misfire-instruction" type="cron-trigger-misfire-instructionType" minOccurs="0" />
+          <xs:element name="cron-expression" type="cron-expressionType" />
+          <xs:element name="time-zone" type="xs:string" minOccurs="0" />
+        </xs:sequence>
+      </xs:extension>
+    </xs:complexContent>
+  </xs:complexType>
+
+  <xs:complexType name="calendarIntervalTriggerType">
+    <xs:annotation>
+      <xs:documentation>Define a DateIntervalTrigger</xs:documentation>
+    </xs:annotation>
+    <xs:complexContent>
+      <xs:extension base="abstractTriggerType">
+        <xs:sequence>
+          <xs:element name="misfire-instruction" type="date-interval-trigger-misfire-instructionType" minOccurs="0" />
+          <xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
+          <xs:element name="repeat-interval-unit" type="interval-unitType" />
+        </xs:sequence>
+      </xs:extension>
+    </xs:complexContent>
+  </xs:complexType>
+
+  <xs:simpleType name="cron-expressionType">
+    <xs:annotation>
+      <xs:documentation>
+        Cron expression (see JavaDoc for examples)
+
+        Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression!
+
+        Regular expressions are not my strong point but I believe this is complete,
+        with the caveat that order for expressions like 3-0 is not legal but will pass,
+        and month and day names must be capitalized.
+        If you want to examine the correctness look for the [\s] to denote the
+        seperation of individual regular expressions. This is how I break them up visually
+        to examine them:
+
+        SECONDS:
+        (
+        ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)
+        | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))
+        | ([\?])
+        | ([\*])
+        ) [\s]
+        MINUTES:
+        (
+        ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)
+        | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))
+        | ([\?])
+        | ([\*])
+        ) [\s]
+        HOURS:
+        (
+        ((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)
+        | (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))
+        | ([\?])
+        | ([\*])
+        ) [\s]
+        DAY OF MONTH:
+        (
+        ((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)
+        | (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)
+        | (L(-[0-9])?)
+        | (L(-[1-2][0-9])?)
+        | (L(-[3][0-1])?)
+        | (LW)
+        | ([1-9]W)
+        | ([1-3][0-9]W)
+        | ([\?])
+        | ([\*])
+        )[\s]
+        MONTH:
+        (
+        ((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)
+        | (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))
+        | (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)
+        | ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))
+        | ([\?])
+        | ([\*])
+        )[\s]
+        DAY OF WEEK:
+        (
+        (([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)
+        | ([1-7]/([1-7]))
+        | (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)
+        | ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)
+        | (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?)
+        | (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)
+        | ([\?])
+        | ([\*])
+        )
+        YEAR (OPTIONAL):
+        (
+        [\s]?
+        ([\*])?
+        | ((19[7-9][0-9])|(20[0-9][0-9]))?
+        | (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?
+        | ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?
+        )
+      </xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:string">
+      <xs:pattern
+        value="(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\?])|([\*]))[\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\?])|([\*]))[\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\?])|([\*]))[\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\?])|([\*]))[\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\?])|([\*]))[\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\?])|([\*]))([\s]?(([\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?| (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?| ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)" />
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:simpleType name="repeat-countType">
+    <xs:annotation>
+      <xs:documentation>Number of times to repeat the Trigger (-1 for indefinite)</xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:integer">
+      <xs:minInclusive value="-1" />
+    </xs:restriction>
+  </xs:simpleType>
+
+
+  <xs:simpleType name="simple-trigger-misfire-instructionType">
+    <xs:annotation>
+      <xs:documentation>Simple Trigger Misfire Instructions</xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:string">
+      <xs:pattern value="SmartPolicy" />
+      <xs:pattern value="RescheduleNextWithExistingCount" />
+      <xs:pattern value="RescheduleNextWithRemainingCount" />
+      <xs:pattern value="RescheduleNowWithExistingRepeatCount" />
+      <xs:pattern value="RescheduleNowWithRemainingRepeatCount" />
+      <xs:pattern value="FireNow" />
+      <xs:pattern value="IgnoreMisfirePolicy" />
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:simpleType name="cron-trigger-misfire-instructionType">
+    <xs:annotation>
+      <xs:documentation>Cron Trigger Misfire Instructions</xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:string">
+      <xs:pattern value="SmartPolicy" />
+      <xs:pattern value="DoNothing" />
+      <xs:pattern value="FireOnceNow" />
+      <xs:pattern value="IgnoreMisfirePolicy" />
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:simpleType name="date-interval-trigger-misfire-instructionType">
+    <xs:annotation>
+      <xs:documentation>Date Interval Trigger Misfire Instructions</xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:string">
+      <xs:pattern value="SmartPolicy" />
+      <xs:pattern value="DoNothing" />
+      <xs:pattern value="FireOnceNow" />
+      <xs:pattern value="IgnoreMisfirePolicy" />
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:simpleType name="interval-unitType">
+    <xs:annotation>
+      <xs:documentation>Interval Units</xs:documentation>
+    </xs:annotation>
+    <xs:restriction base="xs:string">
+      <xs:pattern value="Day" />
+      <xs:pattern value="Hour" />
+      <xs:pattern value="Minute" />
+      <xs:pattern value="Month" />
+      <xs:pattern value="Second" />
+      <xs:pattern value="Week" />
+      <xs:pattern value="Year" />
+    </xs:restriction>
+  </xs:simpleType>
+
+</xs:schema>

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

@@ -4,4 +4,5 @@
   <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" />
+  <package id="Quartz" version="2.6.0" targetFramework="net452" />
 </packages>

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

@@ -26,6 +26,7 @@ using GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes;
 using System.Web.Routing;
 using GreenTree.Nachtragsmanagement.Core.Authentication;
 using GreenTree.Nachtragsmanagement.Services.Appendix;
+using GreenTree.Nachtragsmanagement.Services.Scheduling;
 
 namespace GreenTree.Nachtragsmanagement.Web.Framework
 {
@@ -116,6 +117,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
             builder.RegisterType<WebHelper>().As<IWebHelper>();
             builder.RegisterType<WebAppTypeFinder>().As<ITypeFinder>();
             builder.RegisterType<RoutePublisher>().As<IRoutePublisher>();
+            builder.RegisterType<NotificationScheduler>().As<INotificationScheduler>();
 
             // Register controllers
             builder.RegisterControllers(Assembly.GetCallingAssembly());

+ 4 - 1
GreenTree.Nachtragsmanagement.Web/Controllers/GlobalController.cs

@@ -73,8 +73,11 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         /// Sets the current role of the logged in user and redirects to the home page
         /// </summary>
         /// <param name="roleId">The id of the new role.</param>
-        public ActionResult SetRole(int roleId)
+        public ActionResult SetRole(int roleId = -1)
         {
+            if (roleId == -1)
+                return RedirectToAction("Index", "Home");
+
             var user = _userHelper.FromCookies();
             var role = _userService.GetRoleById(roleId);
 

+ 47 - 6
GreenTree.Nachtragsmanagement.Web/Controllers/MiscController.cs

@@ -8,6 +8,7 @@ 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.Scheduling;
 using GreenTree.Nachtragsmanagement.Services.User;
 using GreenTree.Nachtragsmanagement.Web.Framework.Authorization;
 using GreenTree.Nachtragsmanagement.Web.Models.Global;
@@ -29,15 +30,18 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         private readonly IMiscService _miscService;
         private readonly IUserService _userService;
         private readonly INotificationService _notificationService;
+        private readonly INotificationScheduler _notificationScheduler;
 
         public MiscController(
             IMiscService miscService,
             IUserService userService,
-            INotificationService notificationService)
+            INotificationService notificationService,
+            INotificationScheduler notificationScheduler)
         {
             _miscService = miscService;
             _userService = userService;
             _notificationService = notificationService;
+            _notificationScheduler = notificationScheduler;
 
             ViewData["AllUsers"] = _userService.GetAllUsers();
             ViewData["AllUsersWithRole"] =
@@ -65,7 +69,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         {
             var mailNotifications = _miscService.GetAllMailNotifications();
             var mailNotificationModels = mailNotifications
-                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .Select(u => MailNotificationDataModel.FromMailNotification(
+                    u, false, _notificationService, _notificationScheduler))
                 .ToList();
 
             return View("~/Views/Misc/MailNotifications.cshtml", mailNotificationModels);
@@ -85,7 +90,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
                     JsonRequestBehavior = JsonRequestBehavior.AllowGet
                 };
 
-            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(mailNotification, false, _notificationService);
+            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(
+                mailNotification, false, _notificationService, _notificationScheduler);
             
             return new JsonResult
             {
@@ -102,7 +108,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         {
             var mailNotifications = _miscService.GetAllMailNotifications();
             var mailNotificationModels = mailNotifications
-                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .Select(u => MailNotificationDataModel.FromMailNotification(
+                    u, false, _notificationService, _notificationScheduler))
                 .ToList();
 
             ViewData["ScrollHeight"] = scrollHeight;
@@ -140,7 +147,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
 
             var mailNotifications = _miscService.GetAllMailNotifications();
             var mailNotificationModels = mailNotifications
-                .Select(u => MailNotificationDataModel.FromMailNotification(u, false, _notificationService))
+                .Select(u => MailNotificationDataModel.FromMailNotification(
+                    u, false, _notificationService, _notificationScheduler))
                 .ToList();
 
             var viewContext = new ViewContext();
@@ -169,7 +177,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         public ActionResult EditMailNotification(int id = -1)
         {
             var mailNotification = _miscService.GetMailNotificationById(id);
-            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(mailNotification, true, _notificationService);
+            var mailNotificationModel = MailNotificationDataModel.FromMailNotification(
+                mailNotification, true, _notificationService, _notificationScheduler);
 
             return PartialView("~/Views/Misc/_MailNotificationEditPartial.cshtml", mailNotificationModel);
         }
@@ -188,9 +197,17 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
                         ((IList<User>)ViewData["AllUsers"])
                             .First(r => r.Id == role).Lastname);
 
+                var notificationPlugin = _notificationService.GetNotificationPlugin(mailNotificationModel.NotificationPluginSystemName);
+
+                if (notificationPlugin != null)
+                    mailNotificationModel.NotificationPlugin = notificationPlugin;
+
                 return PartialView("~/Views/Misc/_MailNotificationEditPartial.cshtml", mailNotificationModel);
             }
 
+            if (mailNotificationModel.CronExpression.Split(' ').Length == 5)
+                mailNotificationModel.CronExpression = mailNotificationModel.CronExpression.Insert(0, "0 ");
+
             var selectedUsers = _userService.GetUsersByIds(mailNotificationModel.UserValues.ToArray());
 
             if (mailNotificationModel.Id == -1)
@@ -214,6 +231,8 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
                 _miscService.UpdateMailNotification(mailNotification);
             }
 
+            _notificationScheduler.Start();
+
             return new JsonResult
             {
                 Data = "success"
@@ -238,6 +257,28 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
             };
         }
 
+        /// <summary>
+        /// Processes the specific mailNotification
+        /// </summary>
+        /// <param name="id">MailNotification id.</param>
+        [HttpPost]
+        public ActionResult ProcessMailNotification(int id)
+        {
+            var mailNotification = _miscService.GetMailNotificationById(id);
+
+            if (mailNotification != null)
+            {
+                var notificationPlugin = _notificationService.GetNotificationPlugin(mailNotification.NotificationPluginSystemName);
+
+                notificationPlugin.ProcessNotifications(new[] { mailNotification });
+            }
+
+            return new JsonResult
+            {
+                Data = "success"
+            };
+        }
+
         #endregion
     }
 }

+ 11 - 5
GreenTree.Nachtragsmanagement.Web/Extensions/GridViewSettingsHelper.cs

@@ -703,10 +703,6 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
             {
                 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 =>
@@ -752,7 +748,9 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
                         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>");
+                            "<a href=\"#\" onclick=\"confirmDelete(" + DataBinder.Eval(c.DataItem, "Id") + ")\">Löschen</a><br />");
+                        html.ViewContext.Writer.Write(
+                            "<a href=\"#\" onclick=\"confirmProcess(" + DataBinder.Eval(c.DataItem, "Id") + ")\">Sofort ausführen</a>");
                     });
                     column.SetHeaderTemplateContent(c =>
                     {
@@ -801,6 +799,14 @@ namespace GreenTree.Nachtragsmanagement.Web.Extensions
                     }
                 });
             });
+            s.Columns.Add(column =>
+            {
+                column.Caption = "Nächste Ausführung";
+                column.FieldName = "NextExecutionTime";
+                column.PropertiesEdit.DisplayFormatString = "dd.MM.yyyy";
+                column.MinWidth = 110;
+                column.Width = new Unit(15, UnitType.Percentage);
+            });
 
             s.ClientLayout = (sender, e) =>
             {

+ 6 - 0
GreenTree.Nachtragsmanagement.Web/Global.asax.cs

@@ -29,6 +29,7 @@ using GreenTree.Nachtragsmanagement.Core.Plugins;
 using GreenTree.Nachtragsmanagement.Services.Configuration;
 using GreenTree.Nachtragsmanagement.Core.Domain.Config;
 using GreenTree.Nachtragsmanagement.Services.Misc;
+using GreenTree.Nachtragsmanagement.Services.Scheduling;
 
 namespace GreenTree.Nachtragsmanagement.Web
 {
@@ -59,6 +60,11 @@ namespace GreenTree.Nachtragsmanagement.Web
                 provider.ValidatorFactory = new AppendixValidatorFactory();
             });
 
+            var notificationScheduler = Singleton<IContainer>.Instance.Resolve<INotificationScheduler>();
+
+            if (notificationScheduler != null)
+                notificationScheduler.Start();
+
             DevExpress.Web.ASPxWebControl.CallbackError += Application_Error;
 
             GenerateTestData();

+ 13 - 3
GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj

@@ -58,6 +58,9 @@
     <Reference Include="Common.Logging.Core, Version=3.3.1.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
       <HintPath>..\packages\Common.Logging.Core.3.3.1\lib\net40\Common.Logging.Core.dll</HintPath>
     </Reference>
+    <Reference Include="CronExpressionDescriptor, Version=2.0.2.0, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\CronExpressionDescriptor.2.0.2\lib\netstandard1.1\CronExpressionDescriptor.dll</HintPath>
+    </Reference>
     <Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
       <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll</HintPath>
     </Reference>
@@ -82,12 +85,18 @@
       <HintPath>..\packages\Quartz.2.6.0\lib\net40\Quartz.dll</HintPath>
     </Reference>
     <Reference Include="System" />
+    <Reference Include="System.ComponentModel.Composition" />
     <Reference Include="System.Data" />
     <Reference Include="System.Drawing" />
+    <Reference Include="System.IO.Compression" />
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Net.Http.Formatting, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
       <HintPath>..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll</HintPath>
     </Reference>
+    <Reference Include="System.Numerics" />
+    <Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
+    </Reference>
     <Reference Include="System.Web.DynamicData" />
     <Reference Include="System.Web.Entity" />
     <Reference Include="System.Web.ApplicationServices" />
@@ -217,6 +226,7 @@
     <Content Include="Scripts\globalize.number.js" />
     <Content Include="Scripts\jquery-1.11.3.js" />
     <Content Include="Scripts\jquery-1.11.3.min.js" />
+    <Content Include="Scripts\jquery-cron-min.js" />
     <Content Include="Scripts\jquery-ui-1.11.4.js" />
     <Content Include="Scripts\jquery-ui-1.11.4.min.js" />
     <Content Include="Scripts\jquery.unobtrusive-ajax.js" />
@@ -321,9 +331,8 @@
     <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" />
+    <Compile Include="Implementations\DeviationNotificationPlugin.cs" />
+    <Compile Include="Implementations\AppendixNotificationPlugin.cs" />
     <Compile Include="Controllers\AdminController.cs" />
     <Compile Include="Controllers\AppendixController.cs" />
     <Compile Include="Controllers\AuthController.cs" />
@@ -379,6 +388,7 @@
     <Compile Include="Validation\Deviation\DisturbanceDataModelValidator.cs" />
     <Compile Include="Validation\Deviation\StatusDataModelValidator.cs" />
     <Compile Include="Validation\Deviation\DeviationDataModelValidator.cs" />
+    <Compile Include="Validation\Misc\MailNotificationDataModelValidator.cs" />
     <Compile Include="Validation\Site\SiteDataModelValidator.cs" />
   </ItemGroup>
   <ItemGroup>

+ 26 - 5
GreenTree.Nachtragsmanagement.Web/Scheduling/AppendixNotificationPlugin.cs → GreenTree.Nachtragsmanagement.Web/Implementations/AppendixNotificationPlugin.cs

@@ -11,10 +11,11 @@ using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
 using System.Net.Mail;
 using System.Net;
 using System.Globalization;
+using Quartz;
 
-namespace GreenTree.Nachtragsmanagement.Web.Scheduling
+namespace GreenTree.Nachtragsmanagement.Web.Implementations
 {
-    public class AppendixNotificationPlugin : INotificationPlugin
+    public class AppendixNotificationPlugin : INotificationPlugin, IJob
     {
         #region Services
 
@@ -70,7 +71,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
                         Guid.Parse("2E46B32A-1912-47F9-951E-C8188AA9BA50"),
                         "GreenTree.Nachtragsmanagement.AppendixNotificationPlugin.ProcessNegotiationProtocol",
                         "Verhandlungsprotokolle überprüfen",
-                        "Erstellt automatisch Benachrichtigungen für Nachträge, die nach 2 Wochen die zwar verhandelt sind, " +
+                        "Erstellt automatisch Benachrichtigungen für Nachträge, die nach 2 Wochen zwar verhandelt sind, " +
                         "jedoch noch kein Protokoll aufweisen."
                     )
                 };
@@ -121,6 +122,26 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
             _appendixService = appendixService;
         }
 
+        #region Quartz implmentation
+
+        /// <summary>
+        /// Executes the current job
+        /// </summary>
+        /// <param name="context"></param>
+        public void Execute(IJobExecutionContext context)
+        {
+            if (!context.JobDetail.JobDataMap.ContainsKey("MailNotifications"))
+                return;
+
+            var mailNotifications = context.JobDetail.JobDataMap.Get("MailNotifications") as IEnumerable<MailNotification>;
+
+            if (mailNotifications == null) return;
+
+            ProcessNotifications(mailNotifications);
+        }
+
+        #endregion
+
         #region Processing
 
         /// <summary>
@@ -176,7 +197,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
 
             var appendices = _appendixService.GetAllAppendices()
                 .Where(a => a.OfferingDate.HasValue &&
-                            a.OfferingDate <= DateTime.Now.AddDays(ageDays) &&
+                            (DateTime.Now - a.OfferingDate).Value.Days >= ageDays &&
                             a.StateId == stateConditionId &&
                             a.NegotiationDate == null)
                 .ToList();
@@ -224,7 +245,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Scheduling
 
             var appendices = _appendixService.GetAllAppendices()
                 .Where(a => a.NegotiationDate.HasValue &&
-                            a.NegotiationDate <= DateTime.Now.AddDays(ageDays) &&
+                            (DateTime.Now - a.NegotiationDate).Value.Days >= ageDays &&
                             a.StateId == stateConditionId)
                 .ToList();
 

+ 313 - 0
GreenTree.Nachtragsmanagement.Web/Implementations/DeviationNotificationPlugin.cs

@@ -0,0 +1,313 @@
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
+using GreenTree.Nachtragsmanagement.Services.User;
+using GreenTree.Nachtragsmanagement.Services.Configuration;
+using GreenTree.Nachtragsmanagement.Services.Appendix;
+using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
+using System.Net.Mail;
+using System.Net;
+using System.Globalization;
+using GreenTree.Nachtragsmanagement.Services.Deviation;
+using GreenTree.Nachtragsmanagement.Core.Domain.Deviation;
+using Quartz;
+
+namespace GreenTree.Nachtragsmanagement.Web.Implementations
+{
+    public class DeviationNotificationPlugin : INotificationPlugin, IJob
+    {
+        #region Services
+
+        private readonly IUserService _userService;
+        private readonly IConfigurationService _configurationService;
+        private readonly IDeviationService _deviationService;
+
+        #endregion
+
+        #region Properties
+
+        /// <summary>
+        /// Id
+        /// </summary>
+        public Guid Id
+        {
+            get
+            {
+                return Guid.Parse("77D662E0-F621-4567-9030-2A106533FE06");
+            }
+        }
+
+        /// <summary>
+        /// System name
+        /// </summary>
+        public string SystemName
+        {
+            get
+            {
+                return "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin";
+            }
+        }
+
+        /// <summary>
+        /// List of available notification jobs
+        /// </summary>
+        public List<NotificationJob> AvailableNotificationJobs
+        {
+            get
+            {
+                return new List<NotificationJob>
+                {
+                    new NotificationJob
+                    (
+                        Guid.Parse("144DFF97-4EE4-4B32-967C-C0375D133DCF"),
+                        "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt",
+                        "Eingangsdatum überprüfen",
+                        "Erstellt automatisch Benachrichtigungen für Vertragsabweichungen, die 40 Tagen, bzw. 60 Tagen nach " +
+                        "Einreichung noch keinem Nachtrag zugeordnet sind."
+                    )
+                };
+            }
+        }
+
+        /// <summary>
+        /// Displayed name
+        /// </summary>
+        public string Name
+        {
+            get
+            {
+                return "Vertragsabweichungsbenachrichtigung";
+            }
+        }
+
+        /// <summary>
+        /// Further description on how this plugin works
+        /// </summary>
+        public string Description
+        {
+            get
+            {
+                return
+                    "Erstellt automatisch Benachrichtigungen für Vertragsabweichungen, die 40 Tagen (Stufe 1), bzw. 60 Tagen (Stufe 2)" +
+                    "nach Einreichung noch keinem Nachtrag zugeordnet sind.";
+            }
+        }
+        #endregion
+
+        /// <summary>
+        /// Initializes a new instance of the DeviationNotificationPlugin class
+        /// </summary>
+        public DeviationNotificationPlugin() { }
+
+        /// <summary>
+        /// Initializes a new instance of the DeviationNotificationPlugin class
+        /// </summary>
+        public DeviationNotificationPlugin(
+            IUserService userService,
+            IConfigurationService configurationService,
+            IDeviationService deviationService)
+        {
+            _userService = userService;
+            _configurationService = configurationService;
+            _deviationService = deviationService;
+        }
+
+        #region Quartz implmentation
+
+        /// <summary>
+        /// Executes the current job
+        /// </summary>
+        /// <param name="context"></param>
+        public void Execute(IJobExecutionContext context)
+        {
+            if (!context.JobDetail.JobDataMap.ContainsKey("MailNotifications"))
+                return;
+
+            var mailNotifications = context.JobDetail.JobDataMap.Get("MailNotifications") as IEnumerable<MailNotification>;
+
+            if (mailNotifications == null) return;
+
+            ProcessNotifications(mailNotifications);
+        }
+
+        #endregion
+
+        #region Processing
+
+        /// <summary>
+        /// Process all mail notifications registered for that plugin
+        /// </summary>
+        /// <param name="mailNotifications">The notifications which shall be generated.</param>
+        public void ProcessNotifications(IEnumerable<MailNotification> mailNotifications)
+        {
+            if (mailNotifications == null || !mailNotifications.Any()) return;
+
+            foreach (var notification in mailNotifications)
+            {
+                switch (notification.NotificationJobSystemName)
+                {
+                    case "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt":
+                        {
+                            ProcessDeviationReceiptNotification(notification);
+                        }
+                        break;
+                    default:
+                        continue;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Notifies the corresponding recipients about all deviations whose receipt date is older than N days in two different groups
+        /// </summary>
+        /// <param name="mailNotification">The notification which shall be generated.</param>
+        private void ProcessDeviationReceiptNotification(MailNotification mailNotification)
+        {
+            var ageDaysLevel1 = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt.AgeDaysLevel1", 40);
+
+            var ageDaysLevel2 = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt.AgeDaysLevel2", 60);
+
+            var deviationsLevel1 = _deviationService.GetAllDeviations()
+                .Where(d => !d.AppendixId.HasValue &&
+                            (DateTime.Now - d.ReceiptDate).Value.Days >= ageDaysLevel1 &&
+                            (DateTime.Now - d.ReceiptDate).Value.Days < ageDaysLevel2)
+                .ToList();
+
+            var deviationsLevel2 = _deviationService.GetAllDeviations()
+                .Where(d => !d.AppendixId.HasValue &&
+                            (DateTime.Now - d.ReceiptDate).Value.Days >= ageDaysLevel2)
+                .ToList();
+
+            if (deviationsLevel1.Any() || deviationsLevel2.Any())
+            {
+                var mailBody = GenerateDeviationReceiptMailBody(deviationsLevel1, deviationsLevel2);
+
+                SendNotification(mailNotification, "Autom. Übersicht nicht zugewiesene Vertragsabweichungen", mailBody);
+            }
+        }
+
+        #endregion
+
+        #region Mail body generation
+
+        /// <summary>
+        /// Generates a mail body with a list of all deviations whose receipt date is older than N days in two different groups
+        /// </summary>
+        /// <param name="deviationsLevel1">All deviations matching the level 1 criteria.</param>
+        /// <param name="deviationsLevel2">All deviations matching the level 2 criteria.</param>
+        public string GenerateDeviationReceiptMailBody(IEnumerable<Deviation> deviationsLevel1, IEnumerable<Deviation> deviationsLevel2)
+        {
+            var ageDaysLevel1 = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt.AgeDaysLevel1", 40);
+
+            var ageDaysLevel2 = _configurationService.TryGetConfigItemValue<int>(
+                "GreenTree.Nachtragsmanagement.DeviationNotificationPlugin.ProcessDeviationReceipt.AgeDaysLevel2", 60);
+
+            var template =
+                "<html>" +
+                "   <body>" +
+                "       {0}" +
+                "       {1}" +
+                "   </body>" +
+                "</html>";
+
+            var templateLevel1 =
+                "<h3>Übersicht \"Offene Vertragsabweichungen älter als {0} Tage\"</h3>" +
+                "<p>Folgende Vertragsabweichungen haben ein Einreichdatum älter als {0} Tage, sind aber noch keinen Nachtrag zugeordnet:</p>" +
+                "<ul>" +
+                "   {1}" +
+                "</ul>";
+
+            var templateLevel1List = String.Empty;
+
+            if (deviationsLevel1.Any())
+            {
+                foreach (var deviation in deviationsLevel1)
+                {
+                    templateLevel1List += String.Format(
+                        "<li>Vertragsabweichung <b>\"{0}\"</b> " +
+                        "in Baustelle <b>\"{1}\"</b> - " +
+                        "Einreichdatum: <b>{2:dd.MM.yyyy}</b>",
+                        deviation.CustomNumber, deviation.Site.CustomNumber, deviation.ReceiptDate);
+                }
+            }
+
+            var resultLevel1List = String.Format(templateLevel1, ageDaysLevel1, templateLevel1List);
+
+            var templateLevel2 =
+                "<h3>Übersicht \"Offene Vertragsabweichungen älter als {0} Tage\"</h3>" +
+                "<p>Folgende Vertragsabweichungen haben ein Einreichdatum älter als {0} Tage, sind aber noch keinen Nachtrag zugeordnet:</p>" +
+                "<ul>" +
+                "   {1}" +
+                "</ul>";
+
+            var templateLevel2List = String.Empty;
+
+            if (deviationsLevel2.Any())
+            {
+                foreach (var deviation in deviationsLevel2)
+                {
+                    templateLevel2List += String.Format(
+                        "<li>Vertragsabweichung <b>\"{0}\"</b> " +
+                        "in Baustelle <b>\"{1}\"</b> - " +
+                        "Einreichdatum: <b>{2:dd.MM.yyyy}</b>",
+                        deviation.CustomNumber, deviation.Site.CustomNumber, deviation.ReceiptDate);
+                }
+            }
+
+            var resultLevel2List = String.Format(templateLevel2, ageDaysLevel2, templateLevel2List);
+
+            return String.Format(template, resultLevel1List, resultLevel2List);
+        }
+
+        #endregion
+
+        #region Mail sending
+
+        /// <summary>
+        /// Sends a generated mail body to the specified recipients in the mail notification
+        /// </summary>
+        /// <param name="mailNotification">The mail notification.</param>
+        /// <param name="subject">The mail subject.</param>
+        /// <param name="body">The mail body.</param>
+        public void SendNotification(MailNotification mailNotification, string subject, string body)
+        {
+            if (mailNotification == null) return;
+
+            var mailServerConfig = _configurationService.GetCurrentConfiguration().MailServerElement;
+
+            var smptClient = new SmtpClient(mailServerConfig.SmtpServer, mailServerConfig.Port)
+            {
+                EnableSsl = mailServerConfig.UseSsl,
+                Credentials = new NetworkCredential(
+                    mailServerConfig.Username,
+                    mailServerConfig.Password,
+                    mailServerConfig.Domain)
+            };
+
+            var recipients =
+                mailNotification.Users
+                    .Select(u => u.MailAddress);
+
+            var mailMessage = new MailMessage
+            {
+                IsBodyHtml = true,
+                Subject = subject,
+                Body = body,
+                From = new MailAddress("Nachtragsbenachrichtigung@schweerbau.de")
+            };
+
+            foreach (var recipient in recipients)
+                mailMessage.To.Add(recipient);
+
+            smptClient.Send(mailMessage);
+        }
+
+        #endregion
+    }
+}

+ 11 - 1
GreenTree.Nachtragsmanagement.Web/Models/Misc/MailNotificationDataModel.cs

@@ -1,5 +1,6 @@
 using GreenTree.Nachtragsmanagement.Core.Plugins;
 using GreenTree.Nachtragsmanagement.Services.Misc;
+using GreenTree.Nachtragsmanagement.Services.Scheduling;
 using GreenTree.Nachtragsmanagement.Web.Models.Admin.User;
 using System;
 using System.Collections.Generic;
@@ -13,6 +14,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Models.Misc
         public int Id { get; set; }
         public string CronExpression { get; set; }
         public string CronExpressionDescription { get; set; }
+        public string NextExecutionTime { get; set; }
         public INotificationPlugin NotificationPlugin { get; set; }
         public string NotificationPluginSystemName { get; set; }
         public string NotificationPluginSystemNameDescription { get; set; }
@@ -39,7 +41,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Models.Misc
         }
 
         public static MailNotificationDataModel FromMailNotification(Core.Domain.Misc.MailNotification mailNotificationEntity, 
-            bool newWhenIsNull, INotificationService service)
+            bool newWhenIsNull, INotificationService service, INotificationScheduler scheduler)
         {
             if (mailNotificationEntity == null && newWhenIsNull)
                 return new MailNotificationDataModel
@@ -51,11 +53,19 @@ namespace GreenTree.Nachtragsmanagement.Web.Models.Misc
                 throw new ArgumentNullException("mailNotificationEntity", "Cannot create MailNotificationDataModel from NULL mailNotification entity.");
 
             var notificationPlugin = service.GetNotificationPlugin(mailNotificationEntity.NotificationPluginSystemName);
+            var nextExecutionTime = scheduler.GetNextExecutionOfJob(mailNotificationEntity);
 
             var notificationDataModel = new MailNotificationDataModel
             {
                 Id = mailNotificationEntity.Id,
                 CronExpression = mailNotificationEntity.CronExpression,
+                CronExpressionDescription = 
+                    CronExpressionDescriptor.ExpressionDescriptor.GetDescription(
+                        mailNotificationEntity.CronExpression, 
+                        new CronExpressionDescriptor.Options { Locale = "de" } ),
+                NextExecutionTime = nextExecutionTime == DateTime.MinValue
+                    ? String.Empty
+                    : nextExecutionTime.ToString("dd.MM.yyyy - HH:mm \"Uhr\""),
                 NotificationPlugin = notificationPlugin,
                 NotificationPluginSystemName = mailNotificationEntity.NotificationPluginSystemName,
                 NotificationJobSystemName = mailNotificationEntity.NotificationJobSystemName,

+ 0 - 12
GreenTree.Nachtragsmanagement.Web/Scheduling/JobScheduler.cs

@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Web;
-
-namespace GreenTree.Nachtragsmanagement.Web.Classes
-{
-    public class JobScheduler
-    {
-
-    }
-}

+ 0 - 12
GreenTree.Nachtragsmanagement.Web/Scheduling/JobWorker.cs

@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Web;
-
-namespace GreenTree.Nachtragsmanagement.Web.Classes
-{
-    public class JobWorker
-    {
-
-    }
-}

文件差異過大導致無法顯示
+ 0 - 0
GreenTree.Nachtragsmanagement.Web/Scripts/jquery-cron-min.js


+ 3 - 0
GreenTree.Nachtragsmanagement.Web/Validation/AppendixValidatorFactory.cs

@@ -2,10 +2,12 @@
 using GreenTree.Nachtragsmanagement.Web.Models.Admin.User;
 using GreenTree.Nachtragsmanagement.Web.Models.Appendix;
 using GreenTree.Nachtragsmanagement.Web.Models.Deviation;
+using GreenTree.Nachtragsmanagement.Web.Models.Misc;
 using GreenTree.Nachtragsmanagement.Web.Models.Site;
 using GreenTree.Nachtragsmanagement.Web.Validation.Admin.User;
 using GreenTree.Nachtragsmanagement.Web.Validation.Appendix;
 using GreenTree.Nachtragsmanagement.Web.Validation.Deviation;
+using GreenTree.Nachtragsmanagement.Web.Validation.Misc;
 using GreenTree.Nachtragsmanagement.Web.Validation.Site;
 using System;
 using System.Collections.Generic;
@@ -31,6 +33,7 @@ namespace GreenTree.Nachtragsmanagement.Web.Validation
             validators.Add(typeof(IValidator<StateDataModel>), new StateDataModelValidator());
             validators.Add(typeof(IValidator<CategoryDataModel>), new CategoryDataModelValidator());
             validators.Add(typeof(IValidator<SiteDataModel>), new SiteDataModelValidator());
+            validators.Add(typeof(IValidator<MailNotificationDataModel>), new MailNotificationDataModelValidator());
         }
 
         public override IValidator CreateInstance(Type validatorType)

+ 32 - 0
GreenTree.Nachtragsmanagement.Web/Validation/Misc/MailNotificationDataModelValidator.cs

@@ -0,0 +1,32 @@
+using FluentValidation;
+using GreenTree.Nachtragsmanagement.Web.Models.Deviation;
+using GreenTree.Nachtragsmanagement.Web.Models.Misc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Web.Validation.Misc
+{
+    public class MailNotificationDataModelValidator : AbstractValidator<MailNotificationDataModel>
+    {
+        public MailNotificationDataModelValidator()
+        {
+            RuleFor(m => m.NotificationPluginSystemName)
+                .NotEmpty()
+                    .WithMessage("Eine Benachrichtigungs-Plugin muss gewählt werden.");
+
+            RuleFor(m => m.NotificationJobSystemName)
+                .NotEmpty()
+                    .WithMessage("Eine Benachrichtigungs-Job muss gewählt werden.");
+
+            RuleFor(m => m.CronExpression)
+                .NotEmpty()
+                    .WithMessage("Ein Zeitplan muss ausgewählt werden.");
+
+            RuleFor(m => m.UserValues)
+                .Must(r => r.Count > 0)
+                    .WithMessage("Mind. ein Benutzer muss ausgewählt werden");
+        }
+    }
+}

+ 48 - 3
GreenTree.Nachtragsmanagement.Web/Views/Misc/MailNotifications.cshtml

@@ -6,6 +6,7 @@
 
 <script>
 	var deleteId;
+	var processId;
 	var gridScrollHeight;
 	var gridScrollOffset = 280;
 	var resizeFinished;
@@ -70,8 +71,8 @@
 				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.SetHeaderText(popupControl.GetHeaderText().replace("{mailNotification}", mailNotification.NotificationPluginSystemNameDescription));
+				$(".dialogTextMailNotification").text($(".dialogTextMailNotification").text().replace("{mailNotification}", mailNotification.NotificationPluginSystemNameDescription));
 				popupControl.Show();
 			}
 		});
@@ -94,6 +95,39 @@
 			}
 		});
 	}
+
+	function confirmProcess(id) {
+		if (!id) return;
+		processId = 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(devPopupControlProcessMailNotification);
+				popupControl.SetHeaderText(popupControl.GetHeaderText().replace("{mailNotification}", mailNotification.NotificationPluginSystemNameDescription));
+				$(".dialogTextMailNotificationProcess").text($(".dialogTextMailNotificationProcess").text().replace("{mailNotification}", mailNotification.NotificationPluginSystemNameDescription));
+				popupControl.Show();
+			}
+		});
+	}
+
+	function processMailNotification() {
+		$.ajax({
+			type: "POST",
+			url: '@Url.Action("ProcessMailNotification", "Misc")',
+			data: { Id: processId },
+			success: function (response) {
+				var popupControl = MVCxClientPopupControl.Cast(devPopupControlProcessMailNotification);
+				popupControl.Hide();
+			},
+			error: function () {
+				 alert("error occured");
+			}
+		});
+	}
 </script>
 
 @using (Html.BeginForm("ExportPartialMailNotifications", "Misc", FormMethod.Post, new { id = "mailNotificationExportForm" }))
@@ -106,9 +140,20 @@
 {
 	PopupName = "devPopupControlDeleteMailNotification",
 	Content = "<div class='dialogTextMailNotification' style='padding: 12px'>Sind Sie sicher, dass Sie die Benachrichtigung " +
-			  "\"{mailNotification}\"?</div>",
+			  "\"{mailNotification}\" löschen möchten?</div>",
 	HeaderText = "\"{mailNotification}\" löschen",
 	YesFunction = "function (s, e) { deleteMailNotification(); }",
 	YesButtonName = "devButtonDeleteMailNotificationYes",
 	NoButtonName = "devButtonDeleteMailNotificationNo"
+})
+
+@Html.Partial("~/Views/Shared/_PopupDialogYesNo.cshtml", new GreenTree.Nachtragsmanagement.Web.Models.Global.YesNoDialogModel
+{
+	PopupName = "devPopupControlProcessMailNotification",
+	Content = "<div class='dialogTextMailNotificationProcess' style='padding: 12px'>Sind Sie sicher, dass Sie die Benachrichtigung " +
+			  "\"{mailNotification}\" sofort durchführen möchten?</div>",
+	HeaderText = "\"{mailNotification}\" löschen",
+	YesFunction = "function (s, e) { processMailNotification(); }",
+	YesButtonName = "devButtonProcessMailNotificationYes",
+	NoButtonName = "devButtonProcessMailNotificationNo"
 })

+ 178 - 8
GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationEditPartial.cshtml

@@ -1,4 +1,5 @@
 @using GreenTree.Nachtragsmanagement.Web.Extensions
+@using GreenTree.Nachtragsmanagement.Core.Plugins
 
 @{ 
 	var userContext = GreenTree.Nachtragsmanagement.Core.CommonHelper.UserContext();
@@ -8,10 +9,82 @@
 
 <div class="mailNotificationEditContainer">
 
+	<script src="~/Scripts/jquery-cron-min.js"></script>
 	<script>
 		var textSeparator = ", ";
-		var deleteId = null;
-		var pluginSystemName = "";
+		var pluginSystemName = "@Model.NotificationPluginSystemName";
+		var cronEditor = null;
+		var notificationPluginDictionary = {};
+		var notificationJobDictionary = {};
+
+		@foreach (var notificationPlugin in (IEnumerable<INotificationPlugin>)ViewData["AllNotificationPlugins"])
+		{
+			ViewContext.Writer.WriteLine("notificationPluginDictionary[\"" + notificationPlugin.SystemName + "\"] = \"" + notificationPlugin.Description + "\";");
+
+			foreach (var notificationJob in notificationPlugin.AvailableNotificationJobs)
+			{
+				ViewContext.Writer.WriteLine("notificationJobDictionary[\"" + notificationJob.SystemName + "\"] = \"" + notificationJob.Description + "\";");
+			}
+		}
+
+		cronEditor = $("#cronExpressionEditor").cron({
+			onChange: function () {
+				$("#CronExpression").val($(this).cron("value"));
+			}
+		});
+
+		function changeElementText(selector, textArray) {
+			for (var i = 0; i < textArray.length; i++) {
+				$(selector).contents().filter(function () {
+					return this.nodeType == 3;
+				})[i].nodeValue = textArray[i];
+			}
+		}
+
+		$(document).ready(function () {
+			@if (String.IsNullOrEmpty(Model.CronExpression))
+			{
+				ViewContext.Writer.WriteLine("cronEditor.cron('value', '0 6 * * 1');");
+			}
+			else
+			{
+				ViewContext.Writer.WriteLine("cronEditor.cron('value', '" + Model.CronExpression + "');");
+			}
+			changeElementText(".cron-period", ["Jede(r/s) "]);
+			changeElementText(".cron-block-mins", ["auf der ", " Minute nach der Stunde"]);
+			changeElementText(".cron-block-time", ["um "]);
+			changeElementText(".cron-block-dow", ["am "]);
+			changeElementText(".cron-block-dom", ["am "]);
+			changeElementText(".cron-block-month", [" des "]);
+			$('select[name="cron-period"] > option[value="minute"]').text("Minute");
+			$('select[name="cron-period"] > option[value="hour"]').text("Stunde");
+			$('select[name="cron-period"] > option[value="day"]').text("Tag");
+			$('select[name="cron-period"] > option[value="week"]').text("Woche");
+			$('select[name="cron-period"] > option[value="month"]').text("Monat");
+			$('select[name="cron-period"] > option[value="year"]').text("Jahr");
+			$('select[name="cron-dow"] > option[value="0"]').text("Sonntag");
+			$('select[name="cron-dow"] > option[value="1"]').text("Montag");
+			$('select[name="cron-dow"] > option[value="2"]').text("Dienstag");
+			$('select[name="cron-dow"] > option[value="3"]').text("Mittwoch");
+			$('select[name="cron-dow"] > option[value="4"]').text("Donnerstag");
+			$('select[name="cron-dow"] > option[value="5"]').text("Freitag");
+			$('select[name="cron-dow"] > option[value="6"]').text("Samstag");
+			$('select[name="cron-month"] > option[value="1"]').text("Januar");
+			$('select[name="cron-month"] > option[value="2"]').text("Februar");
+			$('select[name="cron-month"] > option[value="3"]').text("März");
+			$('select[name="cron-month"] > option[value="4"]').text("April");
+			$('select[name="cron-month"] > option[value="5"]').text("Mai");
+			$('select[name="cron-month"] > option[value="6"]').text("Juni");
+			$('select[name="cron-month"] > option[value="7"]').text("Juli");
+			$('select[name="cron-month"] > option[value="8"]').text("August");
+			$('select[name="cron-month"] > option[value="9"]').text("September");
+			$('select[name="cron-month"] > option[value="10"]').text("Oktober");
+			$('select[name="cron-month"] > option[value="11"]').text("November");
+			$('select[name="cron-month"] > option[value="12"]').text("Dezember");
+			for (var i = 1; i <= 31; i++) {
+				$('select[name="cron-dom"] > option[value="' + i + '"]').text(i + ".");
+			}
+		});
 
 		function saveMailNotification() {
 			var form = $("#mailNotificationEditForm");
@@ -24,7 +97,7 @@
 						setTimeout(function () {
 							$(".mailNotificationEditContainer").remove();
 							if (response == "success") {
-								devGridViewMailNotification.PerformCallback();
+								devGridViewMailNotifications.PerformCallback();
 							} else {
 								$("body").append(response);
 							}
@@ -70,8 +143,47 @@
 			}
 			return actualValues;
 		}
+
+		function onNotificationPluginSelectedIndexChanged() {
+			pluginSystemName = NotificationPluginSystemName.GetValue();
+			NotificationJobSystemName.PerformCallback();
+			$("#notificationPluginDescription").text(notificationPluginDictionary[pluginSystemName]);
+		}
+
+		function onNotificationJobSelectedIndexChanged() {
+			var jobSystemName = NotificationJobSystemName.GetValue();
+			$("#notificationJobDescription").text(notificationJobDictionary[jobSystemName]);
+		}
 	</script>
 
+	<style>
+		.notificationDescription {
+			margin-top: 8px;
+			color: #009688;
+			font-style: italic;
+		}
+
+		#cronExpressionEditor {
+			margin-top: 3px;
+			padding-top: 10px;
+			border-top: 1px solid #009688;
+		}
+
+		#cronExpressionEditor select {
+			padding: 6px 4px;
+			border: 1px solid #DCDCDC;
+			border-radius: 2px;
+			-webkit-border-radius: 2px;
+			-moz-border-radius: 2px;
+			-o-border-radius: 2px;
+			-khtml-border-radius: 2px;
+		}
+
+		#cronExpressionEditor select:focus {
+			border: 1px solid #009688;
+		}
+	</style>
+
 	@Html.DevExpress().PopupControl(s =>
 {
 	s.Name = "devPopupControlEditMailNotification";
@@ -79,17 +191,17 @@
 	if (Model.Id == -1)
 		s.HeaderText = "Neue Benachrichtigung erstellen";
 	else
-		s.HeaderText = "\"" + Model.NotificationPluginSystemNameDescription + " - " + Model.NotificationJobSystemNameDescription + "\" bearbeiten";
+		s.HeaderText = "\"" + Model.NotificationPluginSystemNameDescription + "\" bearbeiten";
 
 	s.Modal = true;
-	s.Width = new Unit(600, UnitType.Pixel);
+	s.Width = new Unit(700, 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.ShowMaximizeButton = false;
 	s.ShowFooter = false;
 	s.ShowOnPageLoad = true;
 	s.SetContent(() =>
@@ -99,6 +211,7 @@
 			ViewContext.Writer.Write("<div class='editFormWrapper'>");
 
 			ViewContext.Writer.Write("<input type=\"hidden\" value=\"" + Model.Id + "\" id=\"Id\" name=\"Id\" />");
+			ViewContext.Writer.Write("<input type=\"hidden\" value=\"" + Model.CronExpression + "\" id=\"CronExpression\" name=\"CronExpression\" />");
 
 			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
 			{
@@ -108,15 +221,33 @@
 					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.NotificationPluginSystemName).ToHtmlString());
 					Html.DevExpress().ComboBoxFor(m => m.NotificationPluginSystemName, t =>
 					{
-						t.Width = new Unit(95, UnitType.Percentage);
+						t.Width = new Unit(100, 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(); }";
+						t.Properties.ClientSideEvents.SelectedIndexChanged = "function (s, e) { onNotificationPluginSelectedIndexChanged(); }";
 					}).BindList(ViewData["AllNotificationPlugins"]).Bind(Model.NotificationPluginSystemName).Render();
 				}
 				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
+
+			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
+			{
+				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 100%'>");
+				{
+					if (Model == null || (Model != null && Model.NotificationPlugin == null))
+						ViewContext.Writer.Write("<div id=\"notificationPluginDescription\" class=\"notificationDescription\"></div>");
+					else
+						ViewContext.Writer.Write(
+							"<div id=\"notificationPluginDescription\" class=\"notificationDescription\">" + Model.NotificationPlugin.Description + "</div>");
+				}
+				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
 
+			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
+			{
 				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 50%'>");
 				{
 					ViewContext.Writer.Write(Html.CustomLabelFor(m => m.NotificationJobSystemName, "Benachrichtigungs-Job:"));
@@ -130,6 +261,26 @@
 			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
 			{
 				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 100%'>");
+				{
+					if (Model == null || (Model != null && Model.NotificationPlugin == null))
+						ViewContext.Writer.Write("<div id=\"notificationJobDescription\" class=\"notificationDescription\"></div>");
+					else
+					{
+						var notificationJob = Model.NotificationPlugin.AvailableNotificationJobs
+							.FirstOrDefault(j => j.SystemName == Model.NotificationJobSystemName);
+
+						if (notificationJob != null)
+							ViewContext.Writer.Write(
+								"<div id=\"notificationJobDescription\" class=\"notificationDescription\">" + notificationJob.Description + "</div>");
+					}
+				}
+				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
+
+			ViewContext.Writer.Write("<div class='inlineModelPropertyContainer'>");
+			{
+				ViewContext.Writer.Write("<div class='inlineModelProperty' style='width: 65%'>");
 				{
 					ViewContext.Writer.Write(Html.CustomLabelFor(m => m.UserValues, "Benutzer:"));
 					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.UserValues).ToHtmlString());
@@ -174,6 +325,25 @@
 			}
 			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.CronExpression, "Interval:"));
+					ViewContext.Writer.Write(Html.ValidationMessageFor(m => m.CronExpression).ToHtmlString());
+					ViewContext.Writer.Write("<div id=\"cronExpressionEditor\"></div>");
+					//Html.DevExpress().AppointmentRecurrenceForm(t =>
+					//{
+					//	t.Name = "devRecurrenceFormCronExpresseion";
+					//	t.Width = new Unit(100, UnitType.Percentage);
+					//	t.IsRecurring = true;
+
+					//}).Render();
+				}
+				ViewContext.Writer.Write("</div>");
+			}
+			ViewContext.Writer.Write("</div>");
+
 			ViewContext.Writer.Write("</div>");
 
 			Html.RenderPartial(

+ 5 - 3
GreenTree.Nachtragsmanagement.Web/Views/Misc/_MailNotificationPluginJobsPartial.cshtml

@@ -1,6 +1,6 @@
 @model GreenTree.Nachtragsmanagement.Web.Models.Misc.MailNotificationDataModel
 
-@if (Model == null)
+@if (Model == null || (Model != null && Model.NotificationPlugin == null))
 {
 	@Html.DevExpress().ComboBoxFor(m => m.NotificationJobSystemName, t =>
 	{
@@ -8,8 +8,9 @@
 		t.Properties.ValueField = "SystemName";
 		t.Properties.ValueType = typeof(string);
 		t.Properties.TextField = "Name";
-		t.CallbackRouteValues = new { Controller = "Misc", View = "PartialNotificationPluginJobs" };
+		t.CallbackRouteValues = new { Controller = "Misc", Action = "PartialNotificationPluginJobs" };
 		t.Properties.ClientSideEvents.BeginCallback = "function (s, e) { e.customArgs['pluginSystemName'] = [ pluginSystemName ]; }";
+		t.Properties.ClientSideEvents.SelectedIndexChanged = "function (s, e) { onNotificationJobSelectedIndexChanged(); }";
 	}).GetHtml()
 }
 else
@@ -20,7 +21,8 @@ else
 		t.Properties.ValueField = "SystemName";
 		t.Properties.ValueType = typeof(string);
 		t.Properties.TextField = "Name";
-		t.CallbackRouteValues = new { Controller = "Misc", View = "PartialNotificationPluginJobs" };
+		t.CallbackRouteValues = new { Controller = "Misc", Action = "PartialNotificationPluginJobs" };
 		t.Properties.ClientSideEvents.BeginCallback = "function (s, e) { e.customArgs['pluginSystemName'] = [ pluginSystemName ]; }";
+		t.Properties.ClientSideEvents.SelectedIndexChanged = "function (s, e) { onNotificationJobSelectedIndexChanged(); }";
 	}).BindList(Model.NotificationPlugin.AvailableNotificationJobs).Bind(Model.NotificationJobSystemName).GetHtml()
 }

+ 33 - 0
GreenTree.Nachtragsmanagement.Web/packages.config

@@ -4,6 +4,7 @@
   <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="CronExpressionDescriptor" version="2.0.2" targetFramework="net452" />
   <package id="EntityFramework" version="6.1.3" targetFramework="net452" />
   <package id="FluentValidation" version="7.1.1" targetFramework="net452" />
   <package id="FluentValidation.Mvc5" version="7.1.1" targetFramework="net452" />
@@ -19,8 +20,40 @@
   <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebPages.Data" version="3.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebPages.de" version="3.2.3" targetFramework="net452" />
+  <package id="Microsoft.NETCore.Platforms" version="1.1.0" targetFramework="net452" />
   <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net452" />
+  <package id="NETStandard.Library" version="1.6.1" targetFramework="net452" />
   <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net452" />
   <package id="Quartz" version="2.6.0" targetFramework="net452" />
+  <package id="System.Collections" version="4.3.0" targetFramework="net452" />
+  <package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net452" />
+  <package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net452" />
+  <package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net452" />
+  <package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net452" />
+  <package id="System.Globalization" version="4.3.0" targetFramework="net452" />
+  <package id="System.IO" version="4.3.0" targetFramework="net452" />
+  <package id="System.IO.Compression" version="4.3.0" targetFramework="net452" />
+  <package id="System.Linq" version="4.3.0" targetFramework="net452" />
+  <package id="System.Linq.Expressions" version="4.3.0" targetFramework="net452" />
+  <package id="System.Net.Http" version="4.3.0" targetFramework="net452" />
+  <package id="System.Net.Primitives" version="4.3.0" targetFramework="net452" />
+  <package id="System.ObjectModel" version="4.3.0" targetFramework="net452" />
+  <package id="System.Reflection" version="4.3.0" targetFramework="net452" />
+  <package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net452" />
+  <package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net452" />
+  <package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net452" />
+  <package id="System.Runtime" version="4.3.0" targetFramework="net452" />
+  <package id="System.Runtime.Extensions" version="4.3.0" targetFramework="net452" />
+  <package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net452" />
+  <package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net452" />
+  <package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net452" />
+  <package id="System.Text.Encoding" version="4.3.0" targetFramework="net452" />
+  <package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net452" />
+  <package id="System.Text.RegularExpressions" version="4.3.0" targetFramework="net452" />
+  <package id="System.Threading" version="4.3.0" targetFramework="net452" />
+  <package id="System.Threading.Tasks" version="4.3.0" targetFramework="net452" />
+  <package id="System.Threading.Timer" version="4.3.0" targetFramework="net452" />
+  <package id="System.Xml.ReaderWriter" version="4.3.0" targetFramework="net452" />
+  <package id="System.Xml.XDocument" version="4.3.0" targetFramework="net452" />
   <package id="WebGrease" version="1.5.2" targetFramework="net452" />
 </packages>

部分文件因文件數量過多而無法顯示