diff --git a/.gitignore b/.gitignore index 55a2d688..d05d3c09 100755 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ SuiteCRMAddIn/Documentation/latex/ *.psess *.vsp + +SuiteCRMAddIn/Images/*_old\.png diff --git a/Doxyfile b/Doxyfile index 1e5d478c..a411bdf8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "SuiteCRM Outlook Add-in" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 3.0.19.48 +PROJECT_NUMBER = 3.0.19.75 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/README.md b/README.md index 0199f155..25a89d2d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### What's in this repository -SuiteCRM Outlook Plug-In v 3.0.19.48 +SuiteCRM Outlook Plug-In v 3.0.19.75 This repository has been created to allow community members to collaborate and contribute to the project. diff --git a/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.Designer.cs b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.Designer.cs new file mode 100644 index 00000000..3e9e32f7 --- /dev/null +++ b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.Designer.cs @@ -0,0 +1,139 @@ +namespace SuiteCRMAddIn.Dialogs +{ + partial class ManualSyncContactForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ManualSyncContactForm)); + this.useLabel = new System.Windows.Forms.Label(); + this.resultsTree = new System.Windows.Forms.TreeView(); + this.cancelButton = new System.Windows.Forms.Button(); + this.saveButton = new System.Windows.Forms.Button(); + this.searchText = new System.Windows.Forms.TextBox(); + this.searchButton = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // useLabel + // + this.useLabel.AutoSize = true; + this.useLabel.Location = new System.Drawing.Point(12, 9); + this.useLabel.Name = "useLabel"; + this.useLabel.Size = new System.Drawing.Size(230, 13); + this.useLabel.TabIndex = 0; + this.useLabel.Text = "Use the form below to find records in SuiteCRM"; + // + // resultsTree + // + this.resultsTree.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.resultsTree.CheckBoxes = true; + this.resultsTree.Location = new System.Drawing.Point(15, 56); + this.resultsTree.Name = "resultsTree"; + this.resultsTree.Size = new System.Drawing.Size(257, 165); + this.resultsTree.TabIndex = 3; + this.resultsTree.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.resultsTree_ItemClick); + // + // cancelButton + // + this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cancelButton.Location = new System.Drawing.Point(197, 227); + this.cancelButton.Name = "cancelButton"; + this.cancelButton.Size = new System.Drawing.Size(75, 23); + this.cancelButton.TabIndex = 5; + this.cancelButton.Text = "Cancel"; + this.cancelButton.UseVisualStyleBackColor = true; + this.cancelButton.Click += new System.EventHandler(this.cancelButton_click); + // + // saveButton + // + this.saveButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.saveButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.saveButton.Enabled = false; + this.saveButton.Location = new System.Drawing.Point(116, 227); + this.saveButton.Name = "saveButton"; + this.saveButton.Size = new System.Drawing.Size(75, 23); + this.saveButton.TabIndex = 4; + this.saveButton.Text = "Save"; + this.saveButton.UseVisualStyleBackColor = true; + this.saveButton.Click += new System.EventHandler(this.saveButton_click); + // + // searchText + // + this.searchText.Location = new System.Drawing.Point(15, 30); + this.searchText.Name = "searchText"; + this.searchText.Size = new System.Drawing.Size(176, 20); + this.searchText.TabIndex = 1; + this.searchText.Leave += new System.EventHandler(this.searchButton_click); + this.searchText.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.seachText_PreviewKeyDown); + // + // searchButton + // + this.searchButton.Location = new System.Drawing.Point(197, 30); + this.searchButton.Name = "searchButton"; + this.searchButton.Size = new System.Drawing.Size(75, 23); + this.searchButton.TabIndex = 2; + this.searchButton.Text = "Search"; + this.searchButton.UseVisualStyleBackColor = true; + this.searchButton.Click += new System.EventHandler(this.searchButton_click); + // + // ManualSyncContactForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cancelButton; + this.ClientSize = new System.Drawing.Size(284, 262); + this.Controls.Add(this.searchButton); + this.Controls.Add(this.searchText); + this.Controls.Add(this.saveButton); + this.Controls.Add(this.cancelButton); + this.Controls.Add(this.resultsTree); + this.Controls.Add(this.useLabel); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 300); + this.Name = "ManualSyncContactForm"; + this.Text = "Manually Sync a Contact"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormClosingEvent); + this.Load += new System.EventHandler(this.manualSyncContactsForm_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label useLabel; + private System.Windows.Forms.TreeView resultsTree; + private System.Windows.Forms.Button cancelButton; + private System.Windows.Forms.Button saveButton; + private System.Windows.Forms.TextBox searchText; + private System.Windows.Forms.Button searchButton; + } +} \ No newline at end of file diff --git a/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.cs b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.cs new file mode 100644 index 00000000..ef90a0c0 --- /dev/null +++ b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.cs @@ -0,0 +1,270 @@ +/** + * Outlook integration for SuiteCRM. + * @package Outlook integration for SuiteCRM + * @copyright SalesAgility Ltd http://www.salesagility.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU LESSER GENERAL PUBLIC LICENCE as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENCE + * along with this program; if not, see http://www.gnu.org/licenses + * or write to the Free Software Foundation,Inc., 51 Franklin Street, + * Fifth Floor, Boston, MA 02110-1301 USA + * + * @author SalesAgility + */ + +#region + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using Microsoft.Office.Interop.Outlook; +using SuiteCRMAddIn.BusinessLogic; +using SuiteCRMAddIn.Extensions; +using SuiteCRMAddIn.Helpers; +using SuiteCRMAddIn.Properties; +using SuiteCRMClient.RESTObjects; +using System.Text; + +#endregion + +namespace SuiteCRMAddIn.Dialogs +{ + public partial class ManualSyncContactForm : Form + { + /// + /// The key for the create node. + /// + private static readonly string CreateNodeKey = "Create"; + + /// + /// The key for the contacts node. + /// + private static readonly string ContactsNodeKey = "Contacts"; + + private Dictionary searchResults = new Dictionary(); + + private bool dontClose = false; + + /// + /// The contact we're going to operate on. + /// + ContactItem contactItem = Globals.ThisAddIn.SelectedContacts.First(); + + public ManualSyncContactForm(string searchString) + { + if (contactItem != null) + { + InitializeComponent(); + this.Text = $"Manually sync {contactItem.FullName}"; + searchText.Text = searchString; + } + else + { + throw new System.Exception("No contact selected in ManualSyncContactForm"); + } + } + + private void ClearAndSearch(string target) + { + using (WaitCursor.For(this, true)) + { + searchResults = new Dictionary(); + + resultsTree.Nodes.Clear(); + resultsTree.Nodes.Add(CreateNodeKey, "Create a new Contact"); + + if (!string.IsNullOrWhiteSpace(target)) + { + var contactsNode = resultsTree.Nodes.Add(ContactsNodeKey, "Contacts"); + + SearchAddChildren(target, contactsNode); + + if (contactsNode.Nodes.Count == 0) + { + resultsTree.Nodes.Remove(contactsNode); + resultsTree.Nodes[CreateNodeKey].Checked = true; + saveButton.Enabled = true; + } + } + } + } + + private void SearchAddChildren(string target, TreeNode contactsNode) + { + var tokens = target.Split(" ;:,".ToCharArray()); + + foreach (var token in tokens.Where(x => !string.IsNullOrEmpty(x))) + foreach (var crmContact in SearchHelper.SearchContacts(token)) + searchResults[crmContact.id] = crmContact; + + foreach (var result in searchResults.Values.OrderBy( + x => $"{x.GetValueAsString("last_name")} {x.GetValueAsString("first_name")}")) + { + TreeNode node = contactsNode.Nodes.Add(result.id, CanonicalString(result)); + var contactItem = Globals.ThisAddIn.SelectedContacts.First(); + + if (IsProbablySameItem(result, contactItem)) + { + node.BackColor = ColorTranslator.FromHtml("#a9ea56"); + } + else if (IsPreviouslySyncedItem(result)) + { + node.BackColor = ColorTranslator.FromHtml("#ea6556"); + } + else if (SyncStateManager.Instance.GetExistingSyncState(result) != null) + { + node.BackColor = ColorTranslator.FromHtml("#ea6556"); + } + + contactsNode.Expand(); + } + } + + private bool IsPreviouslySyncedItem(string crmId) + { + return !string.IsNullOrEmpty(crmId) && + searchResults.ContainsKey(crmId) && + IsPreviouslySyncedItem(searchResults[crmId]); + } + + private bool IsPreviouslySyncedItem(EntryValue result) + { + return !string.IsNullOrEmpty(result.GetValueAsString("outlook_id")) || + !string.IsNullOrEmpty(result.GetValueAsString("sync_contact")) || + SyncStateManager.Instance.GetExistingSyncState(result) != null; + } + + private bool IsProbablySameItem(EntryValue result, ContactItem contactItem) + { + return result != null && + (result.id.Equals(contactItem.UserProperties[SyncStateManager.CrmIdPropertyName]?.Value) || + result.GetValueAsString("outlook_id")?.Equals(contactItem.EntryID)); + } + + private static string CanonicalString(EntryValue result) + { + return + $"{result.GetValueAsString("first_name")} {result.GetValueAsString("last_name")} ({result.GetValueAsString("email1")})"; + } + + private void searchButton_click(object sender, EventArgs e) + { + ClearAndSearch(searchText.Text); + } + + private void saveButton_click(object sender, EventArgs e) + { + var crmId = contactItem.GetCrmId().ToString(); + string selectedId = resultsTree.GetAllNodes().FirstOrDefault(x => x.Checked)?.Name; + EntryValue selectedItem = searchResults.ContainsKey(selectedId) ? searchResults[selectedId] : null; + List problems = new List(); + + if (contactItem.Sensitivity == OlSensitivity.olPrivate) + { + problems.Add($"Contact {contactItem.FullName} is marked 'private'. Are you sure?"); + } + + if (resultsTree.Nodes["create"].Checked && IsPreviouslySyncedItem(crmId)) + { + problems.Add($"A record for contact {contactItem.FullName} already exists in CRM. Are you sure you want to create a new record?"); + } + if (selectedItem != null && + !IsProbablySameItem(selectedItem, contactItem)) + { + problems.Add($"The record for {selectedItem.GetValueAsString("first_name")} {selectedItem.GetValueAsString("last_name")} will be overwritten with the details of {contactItem.FullName}."); + } + if (IsPreviouslySyncedItem(crmId) && selectedItem != null) + { + problems.Add($"Contact {selectedItem.GetValueAsString("first_name")} {selectedItem.GetValueAsString("last_name")} has previously been synced and will be overwritten."); + } + + if (resultsTree.Nodes["create"].Checked && + IsPreviouslySyncedItem(crmId) ) + { + problems.Add($"Contact {contactItem.FullName} has previously been synced. Are you sure you want to create another copy?"); + } + + if (problems.Count == 0 || MessageBox.Show( + string.Join("\n", problems.Select(p => $"• {p}\n").ToArray()), + "Problems found: are you sure?", + MessageBoxButtons.OKCancel, + MessageBoxIcon.Warning) == + DialogResult.OK) + { + if (resultsTree.Nodes["create"].Checked) + { + contactItem.ClearCrmId(); + contactItem.SetManualOverride(); + } + else + { + try + { + contactItem.ChangeCrmId(resultsTree.GetAllNodes().FirstOrDefault(x => x.Checked).Name); + } + finally + { + contactItem.SetManualOverride(); + } + } + } + else + { + dontClose = true; + } + } + + private void cancelButton_click(object sender, EventArgs e) + { + Close(); + } + + private void resultsTree_ItemClick(object sender, TreeNodeMouseClickEventArgs e) + { + var contactsNode = resultsTree.Nodes[ContactsNodeKey]; + var createNode = resultsTree.Nodes[CreateNodeKey]; + + if (e.Node == contactsNode) + { + e.Node.Checked = false; + // You can't check the 'Contacts' node. + } + else + { + foreach (var node in resultsTree.GetAllNodes().Where( n => n != e.Node)) + node.Checked = false; + } + + saveButton.Enabled = resultsTree.GetAllNodes().Any(x => x.Checked); + } + + private void manualSyncContactsForm_Load(object sender, EventArgs e) + { + if (Settings.Default.AutomaticSearch) + BeginInvoke((MethodInvoker) delegate { ClearAndSearch(searchText.Text); }); + } + + private void seachText_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) + { + if (e.KeyCode == Keys.Enter && !string.IsNullOrWhiteSpace(searchText.Text)) + ClearAndSearch(searchText.Text); + } + + private void FormClosingEvent(object sender, FormClosingEventArgs e) + { + e.Cancel = dontClose; + dontClose = false; + } + } +} diff --git a/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.resx b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.resx new file mode 100644 index 00000000..45f9e24c --- /dev/null +++ b/SuiteCRMAddIn/Dialogs/ManualSyncContactForm.resx @@ -0,0 +1,408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAEAQEAAAAEAIAAoQgAAFgAAACgAAABAAAAAgAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAADqVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+lWaL0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADqVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP9VAFUDAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADpVmib6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQEAE6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/pVmi9AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADqVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/4AA + gAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAADqVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pWaP/qVmj/6lZo/+pW + aP/qVmj/6lZo/1UAVQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AABWZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GF + d/+RhXf/kYV3/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAFZl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAD/AgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAJGFd/+RhXf/kYV3/5GFd/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAD/AVZl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GFd/+RhXf/kYV3/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAD/AgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJGF + d/+RhXf/kYV3/5GFd/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABWZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GFd/+RhXf/kYV3/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GF + d/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/wAAAAAAAAAAVmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GF + d/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/8AAAAAAAAAAFZl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJGFd/+RhXf/kYV3/5GF + d/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GF + d/+RhXf/AAAAAAAAAABWZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AACRhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GF + d/+RhXf/kYV3/5GFd/+RhXf/kYV3/wAAAAAAAAAAVmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GF + d/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/+RhXf/kYV3/5GFd/8AAAAAAAAAAFZl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAkYV3/5GFd/+RhXf/kYV3/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAVmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAJGFd/+RhXf/kYV3/5GFd/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GF + d/+RhXf/kYV3/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAJGFd/+RhXf/kYV3/5GFd/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAVmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/AAD/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRhXf/kYV3/5GFd/+RhXf/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWZer/VmXq/1Zl6v9WZer/VmXq/1Zl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYV3/5GFd/+RhXf/kYV3/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZl + 6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/VmXq/1Zl6v9WZer/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJGF + d/+RhXf/kYV3/5GFd/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWZer/VmXq/1Zl6v9WZer/VmXq/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA/////////////////////////////////////////////////////////gAAAP/////AAAAA + A////gAAAAAAf//8AAAAAAAf//gAAAAAAA//+AAAAAAAD//4AAAAAAAP//gAAAAAAA//+AAAAAAAD//4 + AAAAAAAP//wAAAAAAA///AAAAAAAH//8AAAAAAAf//4AAAAAAB///gAAAAAAP///AAAAAAA///8AAAAA + AH///4AAAAAA////wAAAAAD////AAAAAAf///+AAAAAD////8AAAAAf////4AAAAD/////4AAAA///// + /wAAAH//////wAAB///////4AA////////////////D//4B/////8P/+AB/////w//gAD/////D/+AAH + ////8P/wAAP////w/+AAA/////D/4AAB////8P/gAAH///AAAMAAAf//8AAAwAAB///wAADAAAH///AA + AMAAAf//8AAAwAAB////8P/gAAH////w/+AAA/////D/4AAD////8P/wAAf////w//gAB/////D//AAf + ////8P/+AD/////w///B//////////////////////////////////////////////////////////// + //////////////////////////////////////////////////8= + + + \ No newline at end of file diff --git a/SuiteCRMAddIn/Extensions/ContactItemExtensions.cs b/SuiteCRMAddIn/Extensions/ContactItemExtensions.cs index a9cb919d..42178ca5 100644 --- a/SuiteCRMAddIn/Extensions/ContactItemExtensions.cs +++ b/SuiteCRMAddIn/Extensions/ContactItemExtensions.cs @@ -24,7 +24,10 @@ namespace SuiteCRMAddIn.Extensions { using BusinessLogic; using SuiteCRMClient; + using System; + using System.Globalization; using System.Runtime.InteropServices; + using System.Xml.Serialization; using Outlook = Microsoft.Office.Interop.Outlook; /// @@ -35,6 +38,16 @@ namespace SuiteCRMAddIn.Extensions /// public static class ContactItemExtensions { + /// + /// Name of the override property. + /// + private const string OverridePropertyName = "UserOverride"; + + /// + /// The duration of the override window in minutes. + /// + private const int OverrideWindowMinutes = 10; + /// /// Remove all the synchronisation properties from this item. /// @@ -70,6 +83,89 @@ public static CrmId GetCrmId(this Outlook.ContactItem olItem) return result; } + /// + /// True if the override window is open for this item. + /// + /// In order to allow manual sync, we need to be able to override the disablement of syncing - + /// but only briefly. + /// The item which we wish to sync. + /// True if the manual sync window is open for this item. + public static bool IsManualOverride(this Outlook.ContactItem olItem) + { + bool result = false; + if (olItem.UserProperties[OverridePropertyName] != null) + { + DateTime value = olItem.UserProperties[OverridePropertyName].Value; + + if ((DateTime.UtcNow - value).Minutes < OverrideWindowMinutes) + { + result = true; + } + else + { + /* no point holding on to a timed-out manual override property */ + olItem.ClearManualOverride(); + } + } + + return result; + } + + /// + /// Set this item as manually syncable, briefly. As a side effect of making the change triggers sync. + /// + /// In order to allow manual sync, we need to be able to override the disablement of syncing - + /// but only briefly. + /// The item which may be synced despite syncing being disabled + public static void SetManualOverride(this Outlook.ContactItem olItem) + { + var p = olItem.UserProperties.Add(OverridePropertyName, Outlook.OlUserPropertyType.olDateTime); + p.Value = DateTime.UtcNow; + olItem.Save(); + } + + /// + /// Clear the manually syncability of this item; does not break is manual sync was not set. + /// + /// In order to allow manual sync, we need to be able to override the disablement of syncing - + /// but only briefly. + /// The item which may be synced despite syncing being disabled + public static void ClearManualOverride(this Outlook.ContactItem olItem) + { + olItem.UserProperties[OverridePropertyName]?.Delete(); + } + + public static void ClearCrmId(this Outlook.ContactItem olItem) + { + var state = SyncStateManager.Instance.GetExistingSyncState(olItem); + + olItem.ClearUserProperty(SyncStateManager.CrmIdPropertyName); + + if (state != null) + { + state.CrmEntryId = null; + } + + olItem.Save(); + } + + public static void ChangeCrmId(this Outlook.ContactItem olItem, string text) + { + var crmId = new CrmId(text); + var state = SyncStateManager.Instance.GetExistingSyncState(olItem); + var userProperty = olItem.UserProperties.Find(SyncStateManager.CrmIdPropertyName) ?? + olItem.UserProperties.Add(SyncStateManager.CrmIdPropertyName, + Outlook.OlUserPropertyType.olText); + userProperty.Value = crmId.ToString(); + + if (state != null) + { + state.CrmEntryId = crmId; + } + + olItem.Save(); + } + /// /// Am I actually a valid Outlook item at all? diff --git a/SuiteCRMAddIn/Extensions/TreeNodeExtensions.cs b/SuiteCRMAddIn/Extensions/TreeNodeExtensions.cs new file mode 100644 index 00000000..1f081c33 --- /dev/null +++ b/SuiteCRMAddIn/Extensions/TreeNodeExtensions.cs @@ -0,0 +1,48 @@ +/** + * Outlook integration for SuiteCRM. + * @package Outlook integration for SuiteCRM + * @copyright SalesAgility Ltd http://www.salesagility.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU LESSER GENERAL PUBLIC LICENCE as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENCE + * along with this program; if not, see http://www.gnu.org/licenses + * or write to the Free Software Foundation,Inc., 51 Franklin Street, + * Fifth Floor, Boston, MA 02110-1301 USA + * + * @author SalesAgility + */ + +#region + +using System.Collections.Generic; +using System.Windows.Forms; + +#endregion + +namespace SuiteCRMAddIn.Extensions +{ + /// + /// Stolen shamelessly from + /// https://stackoverflow.com/questions/4702051/get-a-list-of-all-tree-nodes-in-all-levels-in-treeview-controls + /// + public static class TreeNodeExtensions + { + public static List GetAllNodes(this TreeNode _self) + { + var result = new List(); + result.Add(_self); + foreach (TreeNode child in _self.Nodes) + result.AddRange(child.GetAllNodes()); + return result; + } + } +} \ No newline at end of file diff --git a/SuiteCRMAddIn/Extensions/TreeViewExtensions.cs b/SuiteCRMAddIn/Extensions/TreeViewExtensions.cs new file mode 100644 index 00000000..4d92a7ef --- /dev/null +++ b/SuiteCRMAddIn/Extensions/TreeViewExtensions.cs @@ -0,0 +1,47 @@ +/** + * Outlook integration for SuiteCRM. + * @package Outlook integration for SuiteCRM + * @copyright SalesAgility Ltd http://www.salesagility.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU LESSER GENERAL PUBLIC LICENCE as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENCE + * along with this program; if not, see http://www.gnu.org/licenses + * or write to the Free Software Foundation,Inc., 51 Franklin Street, + * Fifth Floor, Boston, MA 02110-1301 USA + * + * @author SalesAgility + */ + +#region + +using System.Collections.Generic; +using System.Windows.Forms; + +#endregion + +namespace SuiteCRMAddIn.Extensions +{ + /// + /// Stolen shamelessly from + /// https://stackoverflow.com/questions/4702051/get-a-list-of-all-tree-nodes-in-all-levels-in-treeview-controls + /// + public static class TreeViewExtensions + { + public static List GetAllNodes(this TreeView _self) + { + var result = new List(); + foreach (TreeNode child in _self.Nodes) + result.AddRange(child.GetAllNodes()); + return result; + } + } +} \ No newline at end of file diff --git a/SuiteCRMAddIn/Helpers/SearchHelper.cs b/SuiteCRMAddIn/Helpers/SearchHelper.cs new file mode 100644 index 00000000..77b94a99 --- /dev/null +++ b/SuiteCRMAddIn/Helpers/SearchHelper.cs @@ -0,0 +1,85 @@ +/** + * Outlook integration for SuiteCRM. + * @package Outlook integration for SuiteCRM + * @copyright SalesAgility Ltd http://www.salesagility.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU LESSER GENERAL PUBLIC LICENCE as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENCE + * along with this program; if not, see http://www.gnu.org/licenses + * or write to the Free Software Foundation,Inc., 51 Franklin Street, + * Fifth Floor, Boston, MA 02110-1301 USA + * + * @author SalesAgility + */ + +#region + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SuiteCRMAddIn.BusinessLogic; +using SuiteCRMAddIn.Exceptions; +using SuiteCRMClient; +using SuiteCRMClient.RESTObjects; + +#endregion + +namespace SuiteCRMAddIn.Helpers +{ + /// + /// We do search (differently) in too many places. This is an attempt to rationalise it. + /// + public class SearchHelper + { + public static IEnumerable SearchContacts(string token) + { + return Search(ContactSynchroniser.CrmModule, token, + new[] {"first_name", "last_name", "email1" , "sync_contact", "outlook_id" }); + } + + public static IEnumerable Search(string module, string token, IEnumerable fields, + string logicalOperator = "OR") + { + var bob = new StringBuilder("("); + var fieldsArray = fields.ToArray(); + + foreach (var field in fieldsArray) + { + switch (field) + { + case "first_name": + case "last_name": + case "name": + if (field != fieldsArray.First()) + bob.Append($"{logicalOperator} "); + bob.Append($"{module.ToLower()}.{field} ").Append(token.Length < 4 + ? $"= '{token}' " + : $"LIKE '%{token}%' "); + break; + case "email1": + if (field != fieldsArray.First()) + bob.Append($"{logicalOperator} "); + bob.Append( + $"({module.ToLower()}.id in (select eabr.bean_id from email_addr_bean_rel eabr INNER JOIN email_addresses ea on eabr.email_address_id = ea.id where eabr.bean_module = '{module}' and ea.email_address "); + bob.Append(token.Length < 4 ? $"= '{token}'))" : $"LIKE '%{token}%'))"); + break; + } + } + bob.Append(")"); + + var result = RestAPIWrapper.GetEntryList(module, bob.ToString(), 1000, "date_entered DESC", 0, false, fieldsArray) + .entry_list; + + return result; + } + } +} diff --git a/SuiteCRMAddIn/Images/Cancel.png b/SuiteCRMAddIn/Images/Cancel.png index 3d19d8a2..1caee068 100644 Binary files a/SuiteCRMAddIn/Images/Cancel.png and b/SuiteCRMAddIn/Images/Cancel.png differ diff --git a/SuiteCRMAddIn/Images/Cancel.svg b/SuiteCRMAddIn/Images/Cancel.svg new file mode 100644 index 00000000..e421e0f1 --- /dev/null +++ b/SuiteCRMAddIn/Images/Cancel.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/SuiteCRMAddIn/Images/Cancel_old.png b/SuiteCRMAddIn/Images/Cancel_old.png new file mode 100644 index 00000000..3d19d8a2 Binary files /dev/null and b/SuiteCRMAddIn/Images/Cancel_old.png differ diff --git a/SuiteCRMAddIn/Images/Check.png b/SuiteCRMAddIn/Images/Check.png index 89bba903..10dd3ea9 100644 Binary files a/SuiteCRMAddIn/Images/Check.png and b/SuiteCRMAddIn/Images/Check.png differ diff --git a/SuiteCRMAddIn/Images/Check.svg b/SuiteCRMAddIn/Images/Check.svg new file mode 100644 index 00000000..69a9c35c --- /dev/null +++ b/SuiteCRMAddIn/Images/Check.svg @@ -0,0 +1,68 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/SuiteCRMAddIn/Images/Check_old.png b/SuiteCRMAddIn/Images/Check_old.png new file mode 100644 index 00000000..89bba903 Binary files /dev/null and b/SuiteCRMAddIn/Images/Check_old.png differ diff --git a/SuiteCRMAddIn/Images/Search.svg b/SuiteCRMAddIn/Images/Search.svg new file mode 100644 index 00000000..554d4bc1 --- /dev/null +++ b/SuiteCRMAddIn/Images/Search.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + search + + + + + + search + + diff --git a/SuiteCRMAddIn/Images/Search_Button.png b/SuiteCRMAddIn/Images/Search_Button.png index 3b3f443c..1fb0b20e 100644 Binary files a/SuiteCRMAddIn/Images/Search_Button.png and b/SuiteCRMAddIn/Images/Search_Button.png differ diff --git a/SuiteCRMAddIn/Images/Settings.png b/SuiteCRMAddIn/Images/Settings.png index f36cc1de..98522261 100644 Binary files a/SuiteCRMAddIn/Images/Settings.png and b/SuiteCRMAddIn/Images/Settings.png differ diff --git a/SuiteCRMAddIn/Images/Settings.svg b/SuiteCRMAddIn/Images/Settings.svg new file mode 100644 index 00000000..d19194fd --- /dev/null +++ b/SuiteCRMAddIn/Images/Settings.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/SuiteCRMAddIn/Images/Settings_old.png b/SuiteCRMAddIn/Images/Settings_old.png new file mode 100644 index 00000000..f36cc1de Binary files /dev/null and b/SuiteCRMAddIn/Images/Settings_old.png differ diff --git a/SuiteCRMAddIn/Images/manualSyncContact.png b/SuiteCRMAddIn/Images/manualSyncContact.png new file mode 100644 index 00000000..4249f4e8 Binary files /dev/null and b/SuiteCRMAddIn/Images/manualSyncContact.png differ diff --git a/SuiteCRMAddIn/Images/manualSyncContact.xcf b/SuiteCRMAddIn/Images/manualSyncContact.xcf new file mode 100644 index 00000000..e823be45 Binary files /dev/null and b/SuiteCRMAddIn/Images/manualSyncContact.xcf differ diff --git a/SuiteCRMAddIn/Menus/General.xml b/SuiteCRMAddIn/Menus/General.xml new file mode 100644 index 00000000..4276280b --- /dev/null +++ b/SuiteCRMAddIn/Menus/General.xml @@ -0,0 +1,29 @@ + + + + + + + +