201117PX Added MV invitations (basically works but MVs should be tracked as separate Entities in XML later)

This commit is contained in:
phantomix 2020-11-17 22:46:03 +01:00
parent bebb27a786
commit 096706b69c
7 changed files with 515 additions and 6 deletions

View File

@ -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);

View File

@ -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

108
core/MvInvitationProcess.cs Normal file
View File

@ -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<Member> 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<Member> memberList = null;
public MvInvitationProcess(IMvInvitationData data, List<Member> 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;
}
}
}

View File

@ -196,6 +196,8 @@
<Compile Include="view\FormWithOkCancel.cs" />
<Compile Include="core\PaymentReceiptProcess.cs" />
<Compile Include="core\ReplaceReflectEntity.cs" />
<Compile Include="view\frmMvInvitation.cs" />
<Compile Include="core\MvInvitationProcess.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />

View File

@ -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<string> MoneyTransfersIds { get; set; } = new List<string>();
@ -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"

View File

@ -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<Member> lm;
switch(mvInvitation.InvitationGroup)
{
case MvInvitationGroup.AllActiveMembers:
lm = new List<Member>();
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)
{

355
view/frmMvInvitation.cs Normal file
View File

@ -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();
}
}
}