From 096706b69c7c5c9a47a76b77be87925a824b4dca Mon Sep 17 00:00:00 2001 From: phantomix Date: Tue, 17 Nov 2020 22:46:03 +0100 Subject: [PATCH] 201117PX Added MV invitations (basically works but MVs should be tracked as separate Entities in XML later) --- Program.cs | 2 +- core/Cronjob.cs | 2 +- core/MvInvitationProcess.cs | 108 +++++++++++ dezentrale-members.csproj | 2 + model/Member.cs | 5 +- view/frmMain.cs | 47 ++++- view/frmMvInvitation.cs | 355 ++++++++++++++++++++++++++++++++++++ 7 files changed, 515 insertions(+), 6 deletions(-) create mode 100644 core/MvInvitationProcess.cs create mode 100644 view/frmMvInvitation.cs diff --git a/Program.cs b/Program.cs index aeaeed0..29e0e58 100644 --- a/Program.cs +++ b/Program.cs @@ -44,7 +44,7 @@ namespace dezentrale { public class Program { - public static uint VersionNumber { get; private set; } = 0x20083100; + public static uint VersionNumber { get; private set; } = 0x20111700; public static string VersionString { get; private set; } = $"{VersionNumber:x}"; public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); diff --git a/core/Cronjob.cs b/core/Cronjob.cs index 1510ffc..e39705f 100644 --- a/core/Cronjob.cs +++ b/core/Cronjob.cs @@ -253,7 +253,7 @@ namespace dezentrale.core else if (m.AccountBalance > 200 * 100) currentDeptLevel = 1; - if (tsLastMail.TotalDays < 30) + if (tsLastMail.TotalDays < 14) { skipInsufficientNotify = true; } else diff --git a/core/MvInvitationProcess.cs b/core/MvInvitationProcess.cs new file mode 100644 index 0000000..d6acff4 --- /dev/null +++ b/core/MvInvitationProcess.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using dezentrale.model; +using dezentrale.model.money; + +namespace dezentrale.core +{ + public enum MvInvitationGroup + { + AllActiveMembers, + AllMembers, + AllSelectedFromMainWindow, + } + //! \short Data provider for the MV invitation process + public interface IMvInvitationData + { + bool GenerateAuthCode { get; } + string Headline { get; } + string Body { get; } + DateTime MvEventDate { get; } + + + + + + /*List MemberList { get; set; } + string DataTemplate { get; set; } + IntermediateFormat DataFormat { get; set; } + + string OutputDirectory { get; set; } + string FileNamePattern { get; set; } + DateTime StartDate { get; set; } + DateTime EndDate { get; set; } + string StartDateString { get; } + string EndDateString { get; } + bool SendEmail { get; set; }*/ + } + + public class MvInvitationProcess : BackgroundProcess + { + private IMvInvitationData data = null; + private List memberList = null; + + public MvInvitationProcess(IMvInvitationData data, List memberList) + { + this.data = data; + this.memberList = memberList; + + Caption = "Send MV invitation E-Mails"; + Steps = (uint) memberList.Count; //number of members to contact + } + + //random string generation is borrowed from https://stackoverflow.com/questions/1344221 + private static Random random = new Random(); + private static string RandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + + //! \short Run MV invitation process + //! \brief For each selected member, an E-Mail is generated and + //! sent out, defined in @ref FormMail + //! \return true if the generation was successful. + protected override bool Run() + { + uint step = 0; + foreach (Member m in memberList) + { + step++; + try + { + LogTarget.LogLine($"Processing member {m.Nickname}...", LogEvent.ELogLevel.Info, "MvInvitationProcess"); + LogTarget.StepStarted(step, $"Sending mail for member {m.Nickname}"); + + m.StartLogEvent("MvInvitationProcess", LogEvent.eEventType.EMail, Program.config.LocalUser); + if(data.GenerateAuthCode) + { + m.MvAuthenticationCode = RandomString(10); + } + + m.MvEventDate = data.MvEventDate; + FormMail fm = new FormMail() + { + To = $"{m.EMailName} <{m.EMail}>", + Subject = data.Headline, + Body = data.Body, + }; + m.CurrentLog.SubEvents.Add(fm.Send(m)); + m.MvDateInvited = DateTime.Now; + m.SaveToFile(); + + LogTarget.StepCompleted(step, $"Done with member {m.Nickname}", true); + } + catch (Exception ex) + { + LogTarget.LogLine($"Error while processing member {m.Nickname}: {ex.Message}", LogEvent.ELogLevel.Error, "MvInvitationProcess"); + LogTarget.StepCompleted(step, $"Error while processing member {m.Nickname}", false); + } + } + return true; + } + } +} \ No newline at end of file diff --git a/dezentrale-members.csproj b/dezentrale-members.csproj index d80f244..2527d58 100644 --- a/dezentrale-members.csproj +++ b/dezentrale-members.csproj @@ -196,6 +196,8 @@ + + diff --git a/model/Member.cs b/model/Member.cs index ffa5c1e..f163a71 100644 --- a/model/Member.cs +++ b/model/Member.cs @@ -119,6 +119,9 @@ namespace dezentrale.model [XmlElement] public int DebtLevel { get; set; } = 0; [XmlElement] public DateTime LastCronjobBalanceMail { get; set; } = DateTime.Now; [XmlElement] public DateTime LastCronjobReducedFeeMail { get; set; } = DateTime.Now; + [XmlElement] public DateTime MvEventDate { get; set; } + [XmlElement] public DateTime MvDateInvited { get; set; } + [XmlElement] public string MvAuthenticationCode { get; set; } [XmlElement("MoneyTransferId")] public List MoneyTransfersIds { get; set; } = new List(); @@ -295,7 +298,7 @@ namespace dezentrale.model Console.WriteLine($"Member.ApplyMoneyTransfer(): sm={sm.Nickname}"); FormMail fm = new FormMail() { - To = $"{sm.Nickname} <{sm.EMail}>", + To = $"{sm.EMailName} <{sm.EMail}>", Subject = $"Schiefe Zahlung von Mitglied {Number} ({Nickname}, {t.AmountString} {t.Currency})", Body = "s. Betreff.\n" + $"Type = {t.GetType()}\n" diff --git a/view/frmMain.cs b/view/frmMain.cs index 41ae96c..cd81a49 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -44,13 +44,16 @@ namespace dezentrale.view new MenuItem("Members") { MenuItems = { new MenuItem("&Add new member", mnuMain_Members_Add), - new MenuItem("Cronjob all", lstMembers_CronjobAll), + new MenuItem("&Cronjob all", lstMembers_CronjobAll), + new MenuItem("-"), + new MenuItem("&Send MV invitation...", mnuMain_Members_MV_intivation), + new MenuItem("Run MV", mnuMain_Members_MV_run), new MenuItem("-"), #if DEBUG - new MenuItem("Generate Testdata", mnuMain_Members_Generate_Testdata), + new MenuItem("Generate &Testdata", mnuMain_Members_Generate_Testdata), new MenuItem("-"), #endif - new MenuItem("Show numeric info", lstMembers_mnuMain_Members_ShowInfo), + new MenuItem("Show numeric &info", lstMembers_mnuMain_Members_ShowInfo), } }, new MenuItem("Payments") { MenuItems = { @@ -181,7 +184,45 @@ namespace dezentrale.view MemberList.SaveToFiles(toSave); lstMembers.AddEntry(m); } + } + + private void mnuMain_Members_MV_intivation(object sender, EventArgs e) + { + //This process runs in two steps: First, the GUI will ask the user about all details, + //Second, there will be a @ref BackgroundProcess launched that prepares and sends the e-mails. + frmMvInvitation mvInvitation = new frmMvInvitation(); + DialogResult dr = mvInvitation.ShowDialog(); + if (dr == DialogResult.Cancel) return; + { + //prepare member list + List lm; + switch(mvInvitation.InvitationGroup) + { + case MvInvitationGroup.AllActiveMembers: + lm = new List(); + foreach (Member m in Program.members.Entries) + if (m.Status == Member.eStatus.Active) lm.Add(m); + break; + case MvInvitationGroup.AllMembers: + lm = Program.members.Entries; + break; + case MvInvitationGroup.AllSelectedFromMainWindow: + lm = lstMembers.GetSelectedItems(); + break; + default: + MessageBox.Show($"Invalid MvInvitationGroup selected (mvInvitation.InvitationGroup)"); + return; + } + + MvInvitationProcess inv = new MvInvitationProcess(mvInvitation, lm); + frmProcessWithLog frmInv = new frmProcessWithLog(inv, true); + dr = frmInv.ShowDialog(); + } } + private void mnuMain_Members_MV_run(object sender, EventArgs e) + { + } + #if DEBUG private void mnuMain_Members_Generate_Testdata(object sender, EventArgs e) { diff --git a/view/frmMvInvitation.cs b/view/frmMvInvitation.cs new file mode 100644 index 0000000..4b251eb --- /dev/null +++ b/view/frmMvInvitation.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +using dezentrale.core; +using dezentrale.model; + +namespace dezentrale.view +{ + /** + * This form is wizard-alike and contains multiple pages that are + * designed to be worked through in order. + */ + public class frmMvInvitation : FormWithOkCancel, IMvInvitationData + { + //IMvInvitationData + public bool GenerateAuthCode { get; private set; } = false; + public string Headline { get; private set; } = ""; + public string Body { get; private set; } = ""; + public MvInvitationGroup InvitationGroup { get; private set; } = MvInvitationGroup.AllActiveMembers; + public DateTime MvEventDate { get; private set; } + + + TabControl tabControl; + //Page Generic + private ComboBox cbInviteSelection; + private DateTimePicker mvDate, mvTime; + private CheckBox chkGenerateAuthCode; + + private TextBox tbHeadline; + + private TextBox tbIntroductionPassage; + private TextBox tbEventTimeAndPlacePassage; + private TextBox tbAuthCodePassage; + + //Page Agenda + private TextBox tbAgendaPassage; + private TextBox tbAdditionalInfoPassage; + + //Page Preview + private TextBox tbPreviewHeadline; + private TextBox tbPreviewBody; + + + protected Button tabControlNext; + + private bool DataGenerated { get; set; } = false; + + private TabPage BuildPageGeneric() + { + DateTime inTwoWeeks = DateTime.Now.Add(new TimeSpan(14, 0, 0, 0)); + + TabPage page = new TabPage("Generic"); + page.Controls.Add(new Label() + { + Text = "Send invitation to:", + Location = new Point(lm, 0 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(cbInviteSelection = new ComboBox() + { + Location = new Point(lm + 113, 0 * line + tm), + Width = 220, + Anchor = AnchorStyles.Top | AnchorStyles.Left, + DropDownStyle = ComboBoxStyle.DropDownList, + ForeColor = Color.Black, + }); + foreach (MvInvitationGroup group in Enum.GetValues(typeof(MvInvitationGroup))) + cbInviteSelection.Items.Add(group); + cbInviteSelection.SelectedIndex = 0; + + page.Controls.Add(new Label() + { + Text = "Event date+time:", + Location = new Point(lm, 1 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(mvDate = new DateTimePicker() + { + Location = new Point(lm + 113, 1 * line + tm), + Width = 110, + Anchor = AnchorStyles.Top | AnchorStyles.Left, + Format = DateTimePickerFormat.Short, + }); + page.Controls.Add(mvTime = new DateTimePicker() + { + Location = new Point(lm + 243, 1 * line + tm), + Width = 90, + Anchor = AnchorStyles.Top | AnchorStyles.Left, + Format = DateTimePickerFormat.Time, + }); + mvDate.Value = new DateTime(inTwoWeeks.Year, inTwoWeeks.Month, inTwoWeeks.Day, 15, 0, 0); + mvTime.Value = new DateTime(inTwoWeeks.Year, inTwoWeeks.Month, inTwoWeeks.Day, 15, 0, 0); + page.Controls.Add(new Label() + { + Text = "Headline:", + Location = new Point(lm, 2 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbHeadline = new TextBox() + { + Location = new Point(lm + 113, 2 * line + tm), + Width = 580, + Text = "Einladung zur Mitgliederversammlung des dezentrale e.V. am {MvEventDate}" + }); + page.Controls.Add(new Label() + { + Text = "Introduction:", + Location = new Point(lm, 3 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbIntroductionPassage = new TextBox() + { + Location = new Point(0, 4 * line + tm), + Size = new Size(700, 80), + Multiline = true, + ScrollBars = ScrollBars.Both, + //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Hallo {Nickname},\n\nhiermit möchte ich Dich als Mitglied\n" + + "des dezentrale e.V. zur Mitgliederversammlung einladen." + }); + page.Controls.Add(new Label() + { + Text = "Event place+time:", + Location = new Point(lm, 8 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbEventTimeAndPlacePassage = new TextBox() + { + Location = new Point(0, 9 * line + tm), + Size = new Size(700, 50), + Multiline = true, + ScrollBars = ScrollBars.Both, + //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Ort: Räumlichkeiten des dezentrale e.V., Dreilindenstr. 19, 04177 Leipzig\n" + + "Datum und Uhrzeit: {MvEventDate}" + }); + page.Controls.Add(chkGenerateAuthCode = new CheckBox() + { + Location = new Point(0, 12 * line + tm), + Width = 700, + Text = "Generate authentification code for every member", + Checked = true, + }); + page.Controls.Add(new Label() + { + Text = "Auth Code:", + Location = new Point(lm, 13 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbAuthCodePassage = new TextBox() + { + Location = new Point(0, 14 * line + tm), + Size = new Size(700, 50), + Multiline = true, + ScrollBars = ScrollBars.Both, + //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Dein Authentifizierungscode: {MvAuthenticationCode}" + }); + chkGenerateAuthCode.CheckedChanged += (sender, e) => { tbAuthCodePassage.Enabled = chkGenerateAuthCode.Checked; }; + return page; + } + + private TabPage BuildPageAgenda() + { + TabPage page = new TabPage("Agenda"); + + + page.Controls.Add(new Label() + { + Text = "Agenda (one item per line, auto-numbered:", + Location = new Point(lm, 0 * line + tm + labelOffs), + Size = new Size(320, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbAgendaPassage = new TextBox() + { + Location = new Point(0, 1 * line + tm), + Size = new Size(700, 160), + Multiline = true, + ScrollBars = ScrollBars.Both, + //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Bestimmung des Versammlungsleiters sowie Protokollanten\n" + + "Feststellung der ordnungsgemäßen Einberufung\n" + + "Feststellung der Beschlussfähigkeit\n" + + "Genehmigung der Tagesordnung\n" + + "Genehmigung des Protokolls der letzten Mitgliederversammlung\n" + + "Berichte des Vorstandes\n" + + "Entlastung des Vorstandes\n" + + "Neuwahl des Vorstandes\n" + + "Verschiedenes" + }); + + page.Controls.Add(new Label() + { + Text = "Additional information (e.g. Satzung quotes, etc):", + Location = new Point(lm, 8 * line + tm + labelOffs), + Size = new Size(320, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbAdditionalInfoPassage = new TextBox() + { + Location = new Point(0, 9 * line + tm), + Size = new Size(700, 160), + Multiline = true, + ScrollBars = ScrollBars.Both, + //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Bitte denkt daran, dass für die Beschlussfähigkeit 51% der regulären Mitglieder vonnöten sind.\n\n" + + $"Liebe Grüße,\n\n\n{Program.config.LocalUser}" + }); + return page; + } + + private TabPage BuildPagePreview() + { + TabPage page = new TabPage("Preview"); + Button btnGenerateResult; + page.Controls.Add(btnGenerateResult = new Button() + { + Location = new Point(lm, 0 * line + tm + labelOffs), + Width = 220, + Text = "Generate from previous pages" + }); + btnGenerateResult.Click += btnGenerateResult_Click; + page.Controls.Add(new Label() + { + Text = "Headline:", + Location = new Point(lm, 2 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbPreviewHeadline = new TextBox() + { + Location = new Point(lm + 113, 2 * line + tm), + Width = 580, + Text = "", + Enabled = false, + }); + page.Controls.Add(new Label() + { + Text = "Message body:", + Location = new Point(lm, 3 * line + tm + labelOffs), + Size = new Size(110, labelHeight), + TextAlign = ContentAlignment.BottomRight, + }); + page.Controls.Add(tbPreviewBody = new TextBox() + { + Location = new Point(0, 4 * line + tm), + Size = new Size(700, 370), + Multiline = true, + ScrollBars = ScrollBars.Both, + Text = "", + Enabled = false, + }); + return page; + } + public frmMvInvitation() + { + DialogResult = DialogResult.Cancel; + this.StartPosition = FormStartPosition.CenterParent; + this.Size = new System.Drawing.Size(800, 600); + this.Text = "dezentrale-members :: Send MV invitation"; + this.Controls.Add(tabControl = new TabControl() + { + Size = new System.Drawing.Size(this.Width - 16, this.Height - 95), + TabPages = + { + BuildPageGeneric(), + BuildPageAgenda(), + BuildPagePreview(), + }, + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + }); + + this.Controls.Add(tabControlNext = new Button() + { + Text = "Next", + Location = new System.Drawing.Point(this.Width - 240 - rm, this.Height - 57), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + }); + tabControlNext.Click += tabControlNext_Click; + + AddOkCancel(this, btnOK_Click, btnCancel_Click); + + btnOk.Enabled = false; //will be enabled when last pane is active: + tabControl.Selected += tabControl_Selected; + } + + private string GenerateAgenda() + { + string agenda = "Tagesordnung:\n"; + string[] items = tbAgendaPassage.Text.Split('\n'); + for(int i = 0; i < items.Length; i++) + { + agenda += $"{i + 1}. {items[i]}\n"; + } + return agenda; + } + private void GenerateResult() + { + tbPreviewHeadline.Enabled = true; + tbPreviewBody.Enabled = true; + tbPreviewHeadline.Text = tbHeadline.Text; + tbPreviewBody.Text = tbIntroductionPassage.Text + "\n\n" + + tbEventTimeAndPlacePassage.Text + "\n\n" + + (chkGenerateAuthCode.Checked ? (tbAuthCodePassage.Text + "\n\n") : "") + + GenerateAgenda() + "\n\n" + + tbAdditionalInfoPassage.Text + "\n\n"; + DataGenerated = true; + } + private void btnGenerateResult_Click(object sender, EventArgs e) + { + GenerateResult(); + } + + private void tabControl_Selected(object sender, TabControlEventArgs e) + { + btnOk.Enabled = (e.TabPageIndex == tabControl.TabPages.Count - 1); + if (btnOk.Enabled && !DataGenerated) GenerateResult(); + tabControlNext.Enabled = !btnOk.Enabled; + } + private void tabControlNext_Click(object sender, EventArgs e) + { + if (tabControl.SelectedIndex < tabControl.TabPages.Count - 1) + tabControl.SelectedIndex++; + } + + private void btnOK_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.OK; + + GenerateAuthCode = chkGenerateAuthCode.Checked; + Headline = tbPreviewHeadline.Text; + Body = tbPreviewBody.Text; + InvitationGroup = (MvInvitationGroup)cbInviteSelection.SelectedItem; + MvEventDate = new DateTime(mvDate.Value.Year, mvDate.Value.Month, mvDate.Value.Day, + mvTime.Value.Hour, mvTime.Value.Minute, mvTime.Value.Second); + this.Close(); + } + private void btnCancel_Click(object sender, EventArgs e) + { + this.Close(); + } + } +} \ No newline at end of file