From 01f4480a08d3652ac6d74041bec8eb8fcbd78f38 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Fri, 31 Oct 2025 17:13:20 +0100 Subject: [PATCH] MemberAdminWindow: Add feature to export .vcf files --- Elwig/Helpers/Export/VCard.cs | 65 +++++++++++++++++++++++++ Elwig/Helpers/ExportMode.cs | 2 +- Elwig/Services/MemberService.cs | 26 ++++++++++ Elwig/Windows/MemberAdminWindow.xaml | 8 +++ Elwig/Windows/MemberAdminWindow.xaml.cs | 9 ++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Elwig/Helpers/Export/VCard.cs diff --git a/Elwig/Helpers/Export/VCard.cs b/Elwig/Helpers/Export/VCard.cs new file mode 100644 index 0000000..03c2e70 --- /dev/null +++ b/Elwig/Helpers/Export/VCard.cs @@ -0,0 +1,65 @@ +using Elwig.Models.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Elwig.Helpers.Export { + public class VCard : IExporter { + + public static string FileExtension => "vcf"; + + private readonly StreamWriter _writer; + + public VCard(string filename) : this(filename, Utils.UTF8) { } + + public VCard(string filename, Encoding encoding) { + _writer = new StreamWriter(filename, false, encoding); + } + + public void Dispose() { + GC.SuppressFinalize(this); + _writer.Dispose(); + } + + public ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + return _writer.DisposeAsync(); + } + + public async Task ExportAsync(IEnumerable data, IProgress? progress = null) { + progress?.Report(0.0); + int count = data.Count() + 1, i = 0; + + foreach (var row in data) { + var billingAddr = row.BillingAddress != null ? $"ADR;TYPE=work;LANGUAGE=de;LABEL=\"{row.BillingAddress.FullName}\\n{row.BillingAddress.Address}\\n{row.BillingAddress.PostalDest.AtPlz?.Plz} {row.BillingAddress.PostalDest.AtPlz?.Ort.Name}\\nÖsterreich\":;;{row.BillingAddress.Address};{row.BillingAddress.PostalDest.AtPlz?.Ort.Name};;{row.BillingAddress.PostalDest.AtPlz?.Plz};Österreich\r\n" : null; + var tel = string.Join("", row.TelephoneNumbers + .Where(n => n.Type != "fax") + .Select(n => $"TEL;TYPE={(n.Type == "mobile" ? "cell" : "voice")}:{n.Number}\r\n")); + var email = string.Join("", row.EmailAddresses.Select(a => $"EMAIL:{a.Address}\r\n")); + await _writer.WriteLineAsync($""" + BEGIN:VCARD + VERSION:4.0 + UID:mg{row.MgNr}@{App.Client.NameToken.ToLower()}.elwig.at + NOTE:MgNr. {row.MgNr} + FN:{row.AdministrativeName} + N:{row.Name};{row.GivenName};{row.MiddleName};{row.Prefix};{row.Suffix} + KIND:{(row.IsJuridicalPerson ? "org" : "individual")} + ADR{(billingAddr == null ? "" : ";TYPE=home")};LANGUAGE=de;LABEL="{row.Address}\n{row.PostalDest.AtPlz?.Plz} {row.PostalDest.AtPlz?.Ort.Name}\nÖsterreich":;;{row.Address};{row.PostalDest.AtPlz?.Ort.Name};;{row.PostalDest.AtPlz?.Plz};Österreich + {billingAddr}{tel}{email}REV:{row.ModifiedAt.ToUniversalTime():yyyyMMdd\THHmmss\Z} + END:VCARD + """); + progress?.Report(100.0 * ++i / count); + } + + await _writer.FlushAsync(); + progress?.Report(100.0); + } + + public void Export(IEnumerable data, IProgress? progress = null) { + ExportAsync(data, progress).GetAwaiter().GetResult(); + } + } +} diff --git a/Elwig/Helpers/ExportMode.cs b/Elwig/Helpers/ExportMode.cs index b026d00..04906cd 100644 --- a/Elwig/Helpers/ExportMode.cs +++ b/Elwig/Helpers/ExportMode.cs @@ -1,5 +1,5 @@ namespace Elwig.Helpers { public enum ExportMode { - Show, SaveList, SavePdf, Print, Email, Export, Upload + Show, SaveList, SavePdf, Print, Email, Vcf, Export, Upload } } diff --git a/Elwig/Services/MemberService.cs b/Elwig/Services/MemberService.cs index 84634a4..f3ade66 100644 --- a/Elwig/Services/MemberService.cs +++ b/Elwig/Services/MemberService.cs @@ -493,6 +493,32 @@ namespace Elwig.Services { }); Mouse.OverrideCursor = null; } + } else if (mode == ExportMode.Vcf) { + var d = new SaveFileDialog() { + FileName = "Mitglieder.vcf", + DefaultExt = "vcf", + Filter = "vCard-Datei (*.vcf)|*.vcf", + Title = "Kontakte speichern unter - Elwig" + }; + if (d.ShowDialog() == true) { + Mouse.OverrideCursor = Cursors.Wait; + await Task.Run(async () => { + try { + var members = await query + .OrderBy(m => m.MgNr) + .Include(m => m.BillingAddress) + .Include(m => m.TelephoneNumbers) + .Include(m => m.EmailAddresses) + .AsSplitQuery() + .ToListAsync(); + using var exporter = new VCard(d.FileName); + await exporter.ExportAsync(members); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + }); + Mouse.OverrideCursor = null; + } } else if (mode == ExportMode.Export) { var d = new SaveFileDialog() { FileName = subject == ExportSubject.Selected ? $"Mitglied_{vm.SelectedMember?.MgNr}.elwig.zip" : $"Mitglieder_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip", diff --git a/Elwig/Windows/MemberAdminWindow.xaml b/Elwig/Windows/MemberAdminWindow.xaml index a74fd12..7e8f1d1 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml +++ b/Elwig/Windows/MemberAdminWindow.xaml @@ -164,6 +164,14 @@ + + + + + diff --git a/Elwig/Windows/MemberAdminWindow.xaml.cs b/Elwig/Windows/MemberAdminWindow.xaml.cs index 0497284..5eb147b 100644 --- a/Elwig/Windows/MemberAdminWindow.xaml.cs +++ b/Elwig/Windows/MemberAdminWindow.xaml.cs @@ -319,9 +319,11 @@ namespace Elwig.Windows { if (MemberList.SelectedItem is Member m) { Menu_Export_ExportSelected.IsEnabled = !IsEditing && !IsCreating; Menu_Export_UploadSelected.IsEnabled = !IsEditing && !IsCreating && App.Config.SyncUrl != null; + Menu_Contacts_Selected.IsEnabled = !IsEditing && !IsCreating; } else { Menu_Export_ExportSelected.IsEnabled = false; Menu_Export_UploadSelected.IsEnabled = false; + Menu_Contacts_Selected.IsEnabled = false; } } @@ -649,6 +651,13 @@ namespace Elwig.Windows { await ViewModel.GenerateMemberList(MemberService.ExportSubject.Selected, ExportMode.Upload); } + private async void Menu_Contacts_All_Click(object sender, RoutedEventArgs evt) => + await ViewModel.GenerateMemberList(MemberService.ExportSubject.All, ExportMode.Vcf); + private async void Menu_Contacts_Filters_Click(object sender, RoutedEventArgs evt) => + await ViewModel.GenerateMemberList(MemberService.ExportSubject.FromFilters, ExportMode.Vcf); + private async void Menu_Contacts_Selected_Click(object sender, RoutedEventArgs evt) => + await ViewModel.GenerateMemberList(MemberService.ExportSubject.Selected, ExportMode.Vcf); + private async void Menu_List_Order_Click(object sender, RoutedEventArgs evt) { Menu_List.IsSubmenuOpen = true; if (sender == Menu_List_OrderMgNr) {