210409PX Many small improvements & changes:

Fixed FormMail not throwing an exception for failed sending
Fixed MV List not refreshed after changes
Added error handling to all places where FormMail.Send() is used
Refactored MvInvitationProcess to SerialMailProcess<T> in order to be able to send multiple mails via a process window
Some GUI changes to MV window
Added code to start and finish a MV, added Textbox for entering AuthCodes
Added a way to select/deselect entries by code in CustomListView
This commit is contained in:
phantomix 2021-04-09 22:41:31 +02:00
parent 72f4b879e1
commit 3dcb107aae
13 changed files with 637 additions and 193 deletions

View File

@ -94,15 +94,22 @@ namespace dezentrale.core
{
//Send greetings mail,//send mail to vorstand
m.StartLogEvent("Sending greetings", LogEvent.eEventType.Greetings, "automatic");
FormMail
tmp = FormMail.GenerateNewMemberWelcome().ReplaceReflect(m);
LogSubEvent lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to user failed");
tmp = FormMail.GenerateNewMemberVorstand().ReplaceReflect(Program.config.VS).ReplaceReflect(m);
lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to vorstand failed");
try
{
FormMail tmp = FormMail.GenerateNewMemberWelcome().ReplaceReflect(m);
LogSubEvent lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to user failed");
} catch (Exception ex) { throw ex; }
try
{
FormMail tmp = FormMail.GenerateNewMemberVorstand().ReplaceReflect(Program.config.VS).ReplaceReflect(m);
LogSubEvent lse = tmp.Send();
m.CurrentLog.SubEvents.Add(lse);
if (lse.Type == LogEvent.eEventType.Error) throw new Exception("Sending mail to vorstand failed");
} catch(Exception ex) { throw ex; }
}
catch (Exception ex)
{
@ -192,9 +199,22 @@ namespace dezentrale.core
TimeSpan tsLastMail = ProgramStartTime.Subtract(m.LastCronjobReducedFeeMail);
if (ts.TotalDays > 0 && ts.TotalDays > 6) //remind if late and max. once a week
{
m.StartLogEvent($"Reminder to hand in reduced fee prove", LogEvent.eEventType.EMail, "automatic");
m.CurrentLog.SubEvents.Add(FormMail.GenerateReducedFeeReminder().Send(m));
m.LastCronjobReducedFeeMail = ProgramStartTime;
m.StartLogEvent($"Reminder to hand in reduced fee prove", LogEvent.eEventType.EMail, "automatic");
LogSubEvent lse;
try
{
lse = FormMail.GenerateReducedFeeReminder().Send(m);
m.LastCronjobReducedFeeMail = ProgramStartTime;
} catch(Exception ex)
{
lse = new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "CheckReducedFeeValidity",
Details = $"Cannot send reminder mail: {ex.Message}"
};
}
m.CurrentLog.SubEvents.Add(lse);
m.SaveToFile();
}
}
@ -283,8 +303,20 @@ namespace dezentrale.core
if (sm != null)
{
FormMail above200 = FormMail.GenerateBalanceAbove200NotifySM().ReplaceReflect(m);
above200.To = $"{sm.EMailName} <{sm.EMail}>";
m.CurrentLog.SubEvents.Add(above200.Send());
above200.To = $"{sm.EMailName} <{sm.EMail}>";
try
{
m.CurrentLog.SubEvents.Add(above200.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "(balance too high) Can't send Mail to Schatzmeister",
Details = ex.Message,
});
Console.WriteLine($"(balance too high) Can't send Mail to Schatzmeiste: {ex.Message}");
}
}
else
Console.WriteLine("ERROR: CheckBalance_PostDegrade: sm = null");
@ -300,8 +332,20 @@ namespace dezentrale.core
{
if (!skipInsufficientNotify)
{
m.StartLogEvent($"Insufficient amount of payments #1", LogEvent.eEventType.EMail, "automatic");
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify1().Send(m));
m.StartLogEvent($"Insufficient amount of payments #1", LogEvent.eEventType.EMail, "automatic");
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify1().Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #1 notification: {ex.Message}");
}
}
} break;
@ -312,19 +356,61 @@ namespace dezentrale.core
if(debtLevelDecrease)
{
m.StartLogEvent($"Insufficient amount of payments #2", LogEvent.eEventType.EMail, "automatic");
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify2().Send(m));
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberNotify2().Send(m));
} catch (Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 notification: {ex.Message}");
}
if (sm != null)
{
FormMail below2sm = FormMail.GenerateBalanceNegativeNotify2SM().ReplaceReflect(m);
below2sm.To = $"{sm.EMailName} <{sm.EMail}>";
m.CurrentLog.SubEvents.Add(below2sm.Send());
try
{
m.CurrentLog.SubEvents.Add(below2sm.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 SM notification: {ex.Message}");
}
} else
Console.WriteLine("ERROR: CheckBalance_PostDegrade: sm = null");
} else
{
m.StartLogEvent($"Deactivation (insufficient payment)", LogEvent.eEventType.Deactivation, "automatic");
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivation().ReplaceReflect(Program.config.VS).Send(m));
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivationVS().ReplaceReflect(Program.config.VS).Send(m));
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivation().ReplaceReflect(Program.config.VS).Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error, Topic = "Email BalanceNegativeMemberDeactivation", Details = ex.Message,
});
}
try
{
m.CurrentLog.SubEvents.Add(FormMail.GenerateBalanceNegativeMemberDeactivationVS().ReplaceReflect(Program.config.VS).Send(m));
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error, Topic = "Email BalanceNegativeMemberDeactivationVS", Details = ex.Message,
});
}
m.Status = Member.eStatus.Disabled;
}
}

View File

@ -1,100 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using dezentrale.model;
using dezentrale.model.money;
namespace dezentrale.core
{
//! \short Data provider for the MV invitation process
public interface IMvInvitationData
{
/*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 Mv mv= null;
private List<MvInvitedMember> memberList = null;
public MvInvitationProcess(Mv mv, List<MvInvitedMember> memberList)
{
this.mv = mv;
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;
FormMail fm = new FormMail()
{
To = "{EMailName} <{EMail}>",
Subject = mv.InviteHeadline,
Body = mv.InviteBody,
};
fm = fm.ReplaceReflect(mv);
foreach (MvInvitedMember m in memberList)
{
step++;
try
{
LogTarget.LogLine($"Processing member {m.Member.NumberString} - {m.Member.Nickname}...", LogEvent.ELogLevel.Info, "MvInvitationProcess");
LogTarget.StepStarted(step, $"Sending mail for member {m.Member.Nickname}");
m.Member.StartLogEvent("MvInvitationProcess", LogEvent.eEventType.EMail, Program.config.LocalUser);
m.AuthCode = RandomString(10);
m.Member.CurrentLog.SubEvents.Add(fm.Send(m.Member));
m.Member.SaveToFile();
m.InvitationDate = DateTime.Now;
m.Invited = true;
LogTarget.StepCompleted(step, $"Done with member {m.Member.Nickname}", true);
if (mv.Status == Mv.MvStatus.InPreparation)
mv.Status = Mv.MvStatus.InvitationSent;
}
catch (Exception ex)
{
LogTarget.LogLine($"Error while processing member {m.Member.Nickname}: {ex.Message}", LogEvent.ELogLevel.Error, "MvInvitationProcess");
LogTarget.StepCompleted(step, $"Error while processing member {m.Member.Nickname}", false);
break;
}
}
LogTarget.LogLine($"Done.", LogEvent.ELogLevel.Info, "MvInvitationProcess");
return true;
}
}
}

View File

@ -184,7 +184,14 @@ namespace dezentrale.core
//send E-Mail
m.StartLogEvent($"Payment receipts E-Mail ({data.StartDateString} ... {data.EndDateString})",LogEvent.eEventType.EMail, Program.config.LocalUser);
LogSubEvent lse = receiptMail.Send(m, outputs);
try
{
m.CurrentLog.SubEvents.Add(receiptMail.Send(m, outputs));
} catch(Exception ex)
{
LogTarget.LogLine($"Cannot send payment receipts E-Mail: {ex.Message}", LogEvent.ELogLevel.Error);
m.CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
m.SaveToFile(true);
}
} else

94
core/SerialMailProcess.cs Normal file
View File

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using dezentrale.model;
namespace dezentrale.core
{
public class SerialMailProcess<T> : BackgroundProcess
{
private FormMail fm = null;
private List<T> entities = null;
public List<T> EntitiesSuccessful { get; private set; } = new List<T>();
public List<T> EntitiesFailed { get; private set; } = new List<T>();
private Func<FormMail, T, IProcessController, bool> customProcessor = null;
private void Init(FormMail fmArg, List<T> entitiesArg)
{
this.fm = fmArg;
this.entities = entitiesArg;
Caption = "Send E-Mails";
Steps = (uint)entities.Count; //number of members to contact
}
public SerialMailProcess(FormMail fm, List<T> entities)
{
Init(fm, entities);
}
public SerialMailProcess(FormMail fm, List<T> entities, Func<FormMail, T, IProcessController, bool> customProcessor)
{
this.customProcessor = customProcessor;
Init(fm, entities);
}
//! \short Run MV mail 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;
string tType = typeof(T).ToString();
foreach (T t in entities)
{
step++;
try
{
LogTarget.LogLine($"Sending mail for {tType} ({t.ToString()})...", LogEvent.ELogLevel.Info, "SerialMailProcess");
LogTarget.StepStarted(step, $"Sending mail for {tType} {t.ToString()}");
bool success = false;
if(customProcessor == null)
{
try
{
fm.Send(t);
success = true;
} catch(Exception ex)
{
LogTarget.LogLine($"{ex.Message}", LogEvent.ELogLevel.Error, "SerialMailProcess");
success = false;
}
} else
{
success = customProcessor(fm, t, LogTarget);
}
if (success)
{
EntitiesSuccessful.Add(t);
}
else
{
LogTarget.LogLine($"Custom mail process for {tType} {t.ToString()} returned false", LogEvent.ELogLevel.Error, "SerialMailProcess");
EntitiesFailed.Add(t);
}
LogTarget.StepCompleted(step, $"Done with {tType} {t.ToString()}", true);
}
catch (Exception ex)
{
LogTarget.LogLine($"Error while processing {tType} {t.ToString()}: {ex.Message}", LogEvent.ELogLevel.Error, "SerialMailProcess");
LogTarget.StepCompleted(step, $"Error while processing {tType} {t.ToString()}", false);
break;
}
}
LogTarget.LogLine($"Done.", LogEvent.ELogLevel.Info, "SerialMailProcess");
return true;
}
}
}

View File

@ -197,7 +197,7 @@
<Compile Include="core\PaymentReceiptProcess.cs" />
<Compile Include="core\ReplaceReflectEntity.cs" />
<Compile Include="view\frmMv.cs" />
<Compile Include="core\MvInvitationProcess.cs" />
<Compile Include="core\SerialMailProcess.cs" />
<Compile Include="model\Mv.cs" />
<Compile Include="model\svg\SvgFile.cs" />
<Compile Include="model\svg\SvgPath.cs" />

View File

@ -96,8 +96,11 @@ namespace dezentrale.model
//client.Send(Program.config.Smtp.From, src.To, src.Subject, src.Body);
} catch(Exception ex)
{
Console.WriteLine($"FormMail.Send() Error:\n{ex.Message}\n");
ret.Type = LogEvent.eEventType.Error;
Console.WriteLine($"FormMail.Send() Error: {ex.Message}\n");
if(ex.InnerException != null) Console.WriteLine($" inner exception: {ex.InnerException.Message}");
throw ex;
//ret.Type = LogEvent.eEventType.Error;
}
return ret;
@ -580,6 +583,26 @@ namespace dezentrale.model
+ "Dies ist eine automatisch generierte E-Mail.\n"
+ "This is an auto-generated E-Mail.\n",
};
}
public static FormMail GenerateMvFinishedNotification()
{
return new FormMail()
{
To = "{EMailName} <{EMail}>",
Subject = "Mitgliederversammlung beendet",
Body = "Hallo {EMailName}!\n"
+ "\n"
+ "Danke für Deine Teilnahme an der Mitgliederversammlung!\n"
+ "Die MV ging von {StartDateTime} bis {EndDateTime}.\n"
+ "Folgende Einladung zur MV ist hinfällig:\n"
+ "The following MV invitation is cancelled:\n"
+ "Titel/Subject: \"{InviteHeadline}\"\n"
+ "\n"
+ "--\n"
+ "Dies ist eine automatisch generierte E-Mail.\n"
+ "This is an auto-generated E-Mail.\n",
};
}
}
}

View File

@ -212,15 +212,29 @@ namespace dezentrale.model
public void TestMail()
{
FormMail testMail = FormMail.GenerateTestmail();
testMail.Send(this);
try
{
testMail.Send(this);
} catch(Exception ex)
{
Console.WriteLine($"Cannot send mail to <{this.EMail}>: {ex.Message}");
throw ex;
}
}
public void AccountStatusMail(string user = null)
{
StartLogEvent("Member Status Mail", LogEvent.eEventType.EMail, user);
FormMail accountStatusMail = FormMail.GenerateSingleMemberStatusReport();
LogSubEvent lse = accountStatusMail.Send(this);
CurrentLog.SubEvents.Add(lse);
SaveToFile();
try
{
CurrentLog.SubEvents.Add(accountStatusMail.Send(this));
SaveToFile();
} catch(Exception ex)
{
CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
SaveToFile();
throw ex;
}
}
public void ApplyMoneyTransfer(MoneyTransfer t, string user = null)
@ -274,18 +288,27 @@ namespace dezentrale.model
+ $"accountBalance = {AccountBalanceString} (new)\n"
+ $"Next payment for this member is due at {PaymentDueMonth}\n"
+ "\n\n--\n(automatische mail)"
};
LogSubEvent lse = fm.Send();
CurrentLog.SubEvents.Add(lse);
//if(lse.Type == LogEvent.eEventType.Error) { }
};
try
{
CurrentLog.SubEvents.Add(fm.Send());
} catch(Exception ex)
{
CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
}
}
if (paymentNotify)
{
FormMail notify = FormMail.GenerateMemberPaymentNotify(odd).ReplaceReflect(t);
LogSubEvent lse = notify.Send(this);
CurrentLog.SubEvents.Add(lse);
try
{
CurrentLog.SubEvents.Add(notify.Send(this));
} catch(Exception ex)
{
CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
using dezentrale.core;
@ -36,9 +37,13 @@ namespace dezentrale.model
member = m;
MemberNumber = m.Number;
}
public override string ToString()
{
return $"{Member.Number} ({Member.Nickname})";
}
}
public class Mv : XmlLog, IMvInvitationData
public class Mv : XmlLog
{
public enum MvStatus
{
@ -51,18 +56,25 @@ namespace dezentrale.model
private MvStatus status = MvStatus.InPreparation;
private DateTime eventDate;
private DateTime startDateTime;
private DateTime endDateTime;
private string place = "";
private string agenda = "";
private string inviteHeadline = "";
private string inviteBody = "";
private string protocol = "";
[XmlAttribute] public MvStatus Status { get { return status; } set { LogPropertyChange("Status", status, value); status = value; } }
[XmlAttribute] public DateTime EventDate { get { return eventDate; } set { LogPropertyChange("EventDate", eventDate, value); eventDate = value; } }
[XmlAttribute("Started")] public DateTime StartDateTime { get { return startDateTime; } set { LogPropertyChange("StartDateTime", startDateTime, value); startDateTime = value; } }
[XmlAttribute("Ended")] public DateTime EndDateTime { get { return endDateTime; } set { LogPropertyChange("EndDateTime", endDateTime, value); endDateTime = value; } }
[XmlElement] public string Place { get { return place; } set { LogPropertyChange("Place", place, value); place = value; } }
[XmlElement] public string Agenda { get { return agenda; } set { LogPropertyChange("Agenda", agenda, value); agenda = value; } }
[XmlElement] public string InviteHeadline { get { return inviteHeadline; } set { LogPropertyChange("InviteHeadline", inviteHeadline, value); inviteHeadline = value; } }
[XmlElement] public string InviteBody { get { return inviteBody; } set { LogPropertyChange("InviteBody", inviteBody, value); inviteBody = value; } }
[XmlElement] public List<MvInvitedMember> Members { get; set; } = new List<MvInvitedMember>();
[XmlElement] public string Protocol { get { return protocol; } set { LogPropertyChange("Protocol", protocol, value); protocol = value; } }
[XmlElement] public List<Blob> Attachments { get; set; } = new List<Blob>();
@ -129,6 +141,47 @@ namespace dezentrale.model
FinishLogEvent();
}
//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());
}
public SerialMailProcess<MvInvitedMember> GetInvitationMailProcess(List<MvInvitedMember> entities = null)
{
FormMail fm = (new FormMail()
{
To = "{EMailName} <{EMail}>",
Subject = InviteHeadline,
Body = InviteBody,
}).ReplaceReflect(this);
if (entities == null) entities = Members;
SerialMailProcess<MvInvitedMember> p =
new SerialMailProcess<MvInvitedMember>(fm, entities,
(FormMail f, MvInvitedMember m, IProcessController LogTarget) =>
{
m.Member.StartLogEvent("MV invitation mail", LogEvent.eEventType.EMail, Program.config.LocalUser);
try
{
m.Member.CurrentLog.SubEvents.Add(f.Send(m.Member));
m.Member.SaveToFile();
} catch(Exception ex)
{
m.Member.CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email invitation failed", Details = ex.Message });
m.Member.SaveToFile();
LogTarget.LogLine(ex.Message, LogEvent.ELogLevel.Error, "SerialMailProcess::mv.invitation");
return false;
}
m.InvitationDate = DateTime.Now;
m.Invited = true;
return true;
});
return p;
}
public FormMail GetInvitationMail(Member m = null, string authCode = null)
{

View File

@ -192,9 +192,10 @@ namespace dezentrale.view
return lvi;
}
public void AddEntry(T t) { this.Items.Add(UpdateLvi(new ListViewItem(), t)); }
public void UpdateEntry(T t) { UpdateLvi(GetExistingLvi(t), t); }
public void RemoveEntry(T t) { ListViewItem lvi = GetExistingLvi(t); if (lvi != null) this.Items.Remove(lvi); }
public void AddEntry(T t) { this.Items.Add(UpdateLvi(new ListViewItem(), t)); }
public void UpdateEntry(T t) { UpdateLvi(GetExistingLvi(t), t); }
public void RemoveEntry(T t) { ListViewItem lvi = GetExistingLvi(t); if (lvi != null) this.Items.Remove(lvi); }
public void DeSelectEntry(T t, bool sel = true) { ListViewItem lvi = GetExistingLvi(t); if (lvi != null) lvi.Selected = sel; }
public T GetFirstSelectedItem()
{
if (this.SelectedItems.Count < 1) return null;
@ -230,6 +231,14 @@ namespace dezentrale.view
}
return mi;
}
public void ClearMenuItems()
{
while(cm.MenuItems.Count > internalColumns)
{
cm.MenuItems.RemoveAt(0);
}
}
public void LoadFromList(List<T> entries)
{
this.SuspendLayout();

View File

@ -15,6 +15,7 @@ namespace dezentrale.view
protected const int line = 20 + margin; //lineheight
protected const int labelHeight = 14;
protected const int labelOffs = 3;
protected const int groupOffset = 10;
protected List<Button> buttons = new List<Button>();

View File

@ -33,21 +33,21 @@ namespace dezentrale.view
{
Name = "inv",
Display = "Invited",
Width = 32, TextAlign = HorizontalAlignment.Right,
Width = 48, TextAlign = HorizontalAlignment.Right,
CustomToString = ( x => ((MvInvitedMember)x).InvitedString),
},
new ConfigLVDataHandler()
{
Name = "att",
Display = "Invited",
Width = 32, TextAlign = HorizontalAlignment.Right,
Display = "Attended",
Width = 61, TextAlign = HorizontalAlignment.Right,
CustomToString = ( x => ((MvInvitedMember)x).AttendedString),
},
new ConfigLVDataHandler()
{
Name = "date",
Display = "Date",
Width = 130,
Display = "InviteDate",
Width = 88,
CustomToString = ( x => (((MvInvitedMember)x).InvitationDate.Year > 1 ? ((MvInvitedMember)x).InvitationDate.ToString() : "-") ),
},
};

View File

@ -229,6 +229,7 @@ namespace dezentrale.view
this.Close();
}
lstMembers.LoadFromList(Program.members.Entries);
lstMv.LoadFromList(Program.mvList.Entries);
mtv.ClearList();
}
}
@ -499,7 +500,13 @@ namespace dezentrale.view
private void lstMembers_AccountStatusMail(object sender, EventArgs e)
{
Member m = lstMembers.GetFirstSelectedItem();
m?.AccountStatusMail();
try
{
m?.AccountStatusMail();
} catch(Exception ex)
{
MessageBox.Show($"Cannot send account status mail:\r\n{ex.Message}");
}
}
private void lstMembers_Edit(object sender, EventArgs e)

View File

@ -1,9 +1,6 @@
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;
@ -20,7 +17,10 @@ namespace dezentrale.view
public Mv mv { get; set; }
private TabControl tabControl;
private TabPage tabInvitationSettings;
//List with all members and invited yes|no
private LvMvInvitations lvMvInvitations;
private TabPage tabInvitationSettings;
//DateTime
private DateTimePicker mvDate, mvTime;
//Place
@ -31,18 +31,42 @@ namespace dezentrale.view
//Subject
private TextBox tbInviteHeadline;
private TextBox tbInviteBody;
//List with all members and invited yes|no
private LvMvInvitations lvMvInvitations;
//Option to invite selected|all members
private TabPage tabRunningMv;
//List with invited members with authenticated status
private Label lblAttendedStats;
//Textbox to paste auth codes from the users
private TextBox tbAuthCodePaste;
private TextBox tbMvProtocol;
//textbox to hold an external MV protocol URL and/or
//[rich] textbox for MV protocol
//Button to finish MV
//MV attachments (e.g. finished PDF protocol)
private TabPage tabLog;
//! \brief Updates the stats labels for MV attend
private void UpdateAttendedDisplay()
{
int att = 0;
foreach(MvInvitedMember mvi in mv.Members)
{
if (mvi.AttendedMv) att++;
}
float percent;
if (mv.Members.Count > 0)
percent = ((float)att * (float)100 / (float)mv.Members.Count);
else
percent = 0;
lblAttendedStats.Text = $"{att} / {mv.Members.Count} ({Math.Floor(percent)} percent)";
if(percent <= 50)
{
lblAttendedStats.BackColor = System.Drawing.Color.Orange;
} else
{
lblAttendedStats.BackColor = System.Drawing.Color.LightGreen;
}
}
//! \brief UpdateGui() will refresh the GUI components for this
//! form according to the current MV state and visible
//! GUI tab. Having this centralized in one function is
@ -54,15 +78,23 @@ namespace dezentrale.view
{
ClearButtons();
TabPage selectedTab = tabControl.SelectedTab;
lvMvInvitations.ClearMenuItems();
if(selectedTab == tabInvitationSettings)
{
bool enabled = (mv.Status == Mv.MvStatus.InPreparation || mv.Status == Mv.MvStatus.InvitationSent);
Button btn;
btn = AddButton("Invite selected", btnInviteSelected_Click); btn.Enabled = enabled;
btn = AddButton("Invite all", btnInviteAll_Click); btn.Enabled = enabled;
lvMvInvitations.AddMenuItem("Invite selected", btnInviteSelected_Click);
lvMvInvitations.AddMenuItem("Invite all", btnInviteAll_Click);
} else if(selectedTab == tabRunningMv)
{
Button btn;
btn = AddButton("Start MV", btnStartMv_Click); btn.Enabled = (mv.Status == Mv.MvStatus.InvitationSent);
btn = AddButton("End MV", btnFinishMv_Click); btn.Enabled = (mv.Status == Mv.MvStatus.Started);
lvMvInvitations.AddMenuItem("Check IN selected", btnCheckInSelected_Click);
lvMvInvitations.AddMenuItem("Check OUT selected", btnCheckOutSelected_Click);
UpdateAttendedDisplay();
} else if(selectedTab == tabLog)
{
@ -70,12 +102,15 @@ namespace dezentrale.view
AddButton("Close", btnClose_Click);
mvDate.Enabled = mv.Status == Mv.MvStatus.InPreparation;
mvTime.Enabled = mv.Status == Mv.MvStatus.InPreparation;
tbPlace.Enabled = mv.Status == Mv.MvStatus.InPreparation;
tbMvAgenda.Enabled = mv.Status == Mv.MvStatus.InPreparation;
tbInviteHeadline.Enabled = mv.Status == Mv.MvStatus.InPreparation;
tbInviteBody.Enabled = mv.Status == Mv.MvStatus.InPreparation;
mvDate.Enabled = mv.Status == Mv.MvStatus.InPreparation;
mvTime.Enabled = mv.Status == Mv.MvStatus.InPreparation;
tbPlace.ReadOnly = mv.Status != Mv.MvStatus.InPreparation;
tbMvAgenda.ReadOnly = mv.Status != Mv.MvStatus.InPreparation;
tbInviteHeadline.ReadOnly= mv.Status != Mv.MvStatus.InPreparation;
tbInviteBody.ReadOnly = mv.Status != Mv.MvStatus.InPreparation;
tbAuthCodePaste.ReadOnly = mv.Status != Mv.MvStatus.Started;
tbMvProtocol.ReadOnly = mv.Status != Mv.MvStatus.Started;
switch (mv.Status)
{
case Mv.MvStatus.InPreparation:
@ -93,6 +128,11 @@ namespace dezentrale.view
{
UpdateMvData();
if(mv.Status != Mv.MvStatus.InPreparation && mv.Status != Mv.MvStatus.InvitationSent)
{
MessageBox.Show($"MV needs to be in one of these states to invite members:\r\n{Mv.MvStatus.InPreparation}, {Mv.MvStatus.InvitationSent}");
return;
}
if (mvMembers == null || mvMembers.Count < 1)
{
MessageBox.Show("No members to invite!");
@ -145,9 +185,20 @@ namespace dezentrale.view
return;
}
//Spawn invitation process window and run process
MvInvitationProcess invProcess = new MvInvitationProcess(mv, inv);
FormMail fm = (new FormMail()
{
To = "{EMailName} <{EMail}>",
Subject = mv.InviteHeadline,
Body = mv.InviteBody,
}).ReplaceReflect(mv);
SerialMailProcess<MvInvitedMember> invProcess = mv.GetInvitationMailProcess(inv);
frmProcessWithLog frmInvProcess = new frmProcessWithLog(invProcess, true);
frmInvProcess.ShowDialog();
if (mv.Status == Mv.MvStatus.InPreparation)
mv.Status = Mv.MvStatus.InvitationSent;
foreach (MvInvitedMember mvi in inv) lvMvInvitations.UpdateEntry(mvi);
UpdateGui();
}
private void btnInviteSelected_Click(object sender, EventArgs e)
@ -158,6 +209,74 @@ namespace dezentrale.view
{
DoInvitation(mv.Members);
}
private void btnStartMv_Click(object sender, EventArgs e)
{
if (mv.Status != Mv.MvStatus.InvitationSent)
{
MessageBox.Show($"MV must be in state {Mv.MvStatus.InvitationSent}!");
return;
}
DateTime now = DateTime.Now;
DateTime evt = mv.EventDate;
if(now.Year != evt.Year || now.Month != evt.Month || now.Day != evt.Day || evt.CompareTo(now) > 0)
{
MessageBox.Show("Today must have the same Date as announced in the EventDate, and Time has to be after the announced time.");
return;
}
mv.Status = Mv.MvStatus.Started;
mv.StartDateTime = now;
//Todo: init protocol with contents from invitation mail
mv.Protocol = mv.AgendaNumberedString;
tbMvProtocol.Text = mv.Protocol;
MessageBox.Show("MV started.");
UpdateGui();
}
private void btnFinishMv_Click(object sender, EventArgs e)
{
if (mv.Status != Mv.MvStatus.Started)
{
MessageBox.Show($"MV must be in state {Mv.MvStatus.Started}!");
return;
}
mv.Status = Mv.MvStatus.Ended;
mv.EndDateTime = DateTime.Now;
MessageBox.Show("MV ended");
//Ask for sending end mail to attended users
UpdateGui();
}
private void btnCheckInSelected_Click(object sender, EventArgs e)
{
if (mv.Status != Mv.MvStatus.Started)
{
MessageBox.Show($"MV must be in state {Mv.MvStatus.Started}!");
return;
}
//CheckIn(lvMvInvitations.GetSelectedItems());
foreach (MvInvitedMember mvi in lvMvInvitations.GetSelectedItems())
{
mvi.AttendedMv = true;
lvMvInvitations.UpdateEntry(mvi);
}
UpdateAttendedDisplay();
}
private void btnCheckOutSelected_Click(object sender, EventArgs e)
{
if (mv.Status != Mv.MvStatus.Started)
{
MessageBox.Show($"MV must be in state {Mv.MvStatus.Started}!");
return;
}
//CheckIn(lvMvInvitations.GetSelectedItems());
foreach (MvInvitedMember mvi in lvMvInvitations.GetSelectedItems())
{
mvi.AttendedMv = false;
lvMvInvitations.UpdateEntry(mvi);
}
UpdateAttendedDisplay();
}
private void UpdateMvData()
@ -171,101 +290,221 @@ namespace dezentrale.view
mv.Agenda = tbMvAgenda.Text;
mv.InviteHeadline = tbInviteHeadline.Text;
mv.InviteBody = tbInviteBody.Text;
mv.Protocol = tbMvProtocol.Text;
}
private void BuildPageInvitationSettings(Control parent)
{
parent.Controls.Add(new Label()
SplitContainer sc = new SplitContainer()
{
Orientation = Orientation.Horizontal,
Dock = DockStyle.Fill,
};
parent.Controls.Add(sc);
GroupBox details = new GroupBox()
{
Text = "Invitation details",
Dock = DockStyle.Fill,
};
sc.Panel1.Controls.Add(details);
GroupBox mail = new GroupBox()
{
Text = "Invitation Mail",
Dock = DockStyle.Fill,
};
sc.Panel2.Controls.Add(mail);
details.Controls.Add(new Label()
{
Text = "Event date+time:",
Location = new Point(lm, 0 * line + tm + labelOffs),
Location = new Point(lm, 0 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(mvDate = new DateTimePicker()
details.Controls.Add(mvDate = new DateTimePicker()
{
Location = new Point(lm + 113, 0 * line + tm),
Location = new Point(lm + 113, 0 * line + tm + groupOffset),
Width = 110,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
Format = DateTimePickerFormat.Short,
});
parent.Controls.Add(mvTime = new DateTimePicker()
details.Controls.Add(mvTime = new DateTimePicker()
{
Location = new Point(lm + 243, 0 * line + tm),
Location = new Point(lm + 243, 0 * line + tm + groupOffset),
Width = 90,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
Format = DateTimePickerFormat.Time,
});
parent.Controls.Add(new Label()
details.Controls.Add(new Label()
{
Text = "Place:",
Location = new Point(lm, 1 * line + tm + labelOffs),
Location = new Point(lm, 1 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbPlace = new TextBox()
details.Controls.Add(tbPlace = new TextBox()
{
Location = new Point(lm + 113, 1 * line + tm),
Width = 580,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
Location = new Point(lm + 113, 1 * line + tm + groupOffset),
Width = details.ClientSize.Width - lm - 113 - groupOffset,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
});
parent.Controls.Add(new Label()
details.Controls.Add(new Label()
{
Text = "Agenda (one item per line, auto-numbered:",
Location = new Point(lm, 2 * line + tm + labelOffs),
Location = new Point(lm, 2 * line + tm + labelOffs + groupOffset),
Size = new Size(320, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbMvAgenda = new TextBox()
details.Controls.Add(tbMvAgenda = new TextBox()
{
Location = new Point(0, 3 * line + tm),
Size = new Size(700, 160),
Location = new Point(groupOffset, 3 * line + tm + groupOffset),
Size = new Size(details.ClientSize.Width - 2 * groupOffset, details.ClientSize.Height - 3 * line - tm - 2 * groupOffset),
Multiline = true,
ScrollBars = ScrollBars.Both,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
});
parent.Controls.Add(new Label()
mail.Controls.Add(new Label()
{
Text = "Headline:",
Location = new Point(lm, 10 * line + tm + labelOffs),
Location = new Point(lm, 0 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbInviteHeadline = new TextBox()
mail.Controls.Add(tbInviteHeadline = new TextBox()
{
Location = new Point(lm + 113, 10 * line + tm),
Width = 580,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
Location = new Point(lm + 113, 0 * line + tm + groupOffset),
Width = details.ClientSize.Width - lm - 113 - groupOffset,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
});
parent.Controls.Add(new Label()
mail.Controls.Add(new Label()
{
Text = "Invitation Body:",
Location = new Point(lm, 11 * line + tm + labelOffs),
Location = new Point(lm, 1 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbInviteBody = new TextBox()
mail.Controls.Add(tbInviteBody = new TextBox()
{
Location = new Point(0, 12 * line + tm),
Size = new Size(700, 160),
Location = new Point(groupOffset, 2 * line + tm + groupOffset),
Size = new Size(details.ClientSize.Width - 2 * groupOffset, details.ClientSize.Height - 2 * line - tm - 2 * groupOffset),
Multiline = true,
ScrollBars = ScrollBars.Both,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
//Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
});
}
private void BuildPageRunningMv(Control parent)
{
//input field to add auth codes
SplitContainer splitLR = new SplitContainer()
{
Dock = DockStyle.Fill,
};
parent.Controls.Add(splitLR);
//input field to add auth codes, with button
SplitContainer splitL_UD = new SplitContainer()
{
Orientation = Orientation.Horizontal,
Dock = DockStyle.Fill,
};
splitLR.Panel1.Controls.Add(splitL_UD);
GroupBox groupAttending = new GroupBox()
{
Text = "Attending members",
Dock = DockStyle.Fill,
Margin = new Padding(5),
};
splitL_UD.Panel1.Controls.Add(groupAttending);
GroupBox groupAttachments = new GroupBox()
{
Text = "Attachments",
Dock = DockStyle.Fill,
Margin = new Padding(5),
};
splitL_UD.Panel2.Controls.Add(groupAttachments);
GroupBox groupProtocol = new GroupBox()
{
Text = "MV protocol",
Dock = DockStyle.Fill,
};
splitLR.Panel2.Controls.Add(groupProtocol);
groupAttending.Controls.Add(new Label()
{
Text = "stats:",
Location = new Point(lm, 0 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
groupAttending.Controls.Add(lblAttendedStats = new Label()
{
Text = "",
Location = new Point(lm + 113, 0 * line + tm + labelOffs + groupOffset),
Size = new Size(110, labelHeight),
});
groupAttending.Controls.Add(new Label()
{
Text = "paste Auth Codes:",
Location = new Point(lm, 1 * line + tm + labelOffs + groupOffset),
Size = new Size(160, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
groupAttending.Controls.Add(tbAuthCodePaste = new TextBox()
{
Multiline = true,
ScrollBars = ScrollBars.Both,
Location = new Point(10, 2 * line + tm + groupOffset),
Size = new Size(groupAttending.Width - 20, groupAttending.Height - 75),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
});
tbAuthCodePaste.KeyPress += tbAuthCodePaste_KeyPress;
groupProtocol.Controls.Add(tbMvProtocol = new TextBox()
{
Multiline = true,
ScrollBars = ScrollBars.Both,
Location = new Point(10, 5 + groupOffset),
Size = new Size(groupProtocol.Width - 20, groupProtocol.Height - 25),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
});
}
private void tbAuthCodePaste_KeyPress(object sender, KeyPressEventArgs e)
{
if (mv.Status != Mv.MvStatus.Started) return;
if (e.KeyChar != 13) return;
bool foundAuthCode = false;
foreach(MvInvitedMember mvi in mv.Members)
{
if (tbAuthCodePaste.Text.Contains(mvi.AuthCode))
{
foundAuthCode = true;
mvi.AttendedMv = true;
//Add mvi to selection in list
//lvMvInvitations.SelectedItems
lvMvInvitations.DeSelectEntry(mvi, true);
}
}
tbAuthCodePaste.Text = "";
if(foundAuthCode)
{
}
}
private void BuildLog(Control parent)
{
parent.Controls.Add(new LogView(mv.Log)
@ -298,6 +537,7 @@ namespace dezentrale.view
grpInvitations.Controls.Add(lvMvInvitations = new LvMvInvitations()
{
Dock = DockStyle.Fill,
HideSelection = false,
});
split.Panel1.Controls.Add(grpInvitations);
@ -338,7 +578,7 @@ namespace dezentrale.view
this.StartPosition = FormStartPosition.CenterParent;
this.Size = new System.Drawing.Size(742, 600);
this.Size = new System.Drawing.Size(1200, 800);
this.Text = "dezentrale-members :: MV administration";
this.FormClosing += frmMv_Closing;
BuildGui(this);
@ -354,6 +594,7 @@ namespace dezentrale.view
tbInviteHeadline.Text = mv.InviteHeadline;
tbInviteBody.Text = mv.InviteBody;
tbMvProtocol.Text = mv.Protocol;
UpdateGui();