diff --git a/src/OutlookAddin.UI/Dialogs/ComposeFromCaseDialog.xaml b/src/OutlookAddin.UI/Dialogs/ComposeFromCaseDialog.xaml index 3e004d5..b6d0d92 100644 --- a/src/OutlookAddin.UI/Dialogs/ComposeFromCaseDialog.xaml +++ b/src/OutlookAddin.UI/Dialogs/ComposeFromCaseDialog.xaml @@ -1,9 +1,10 @@ + @@ -134,24 +136,30 @@ - - - - - - - - - - - - - + + + + + + + + (); foreach (var hit in search.List) { - if (!string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase)) continue; - Cases.Add(new CaseEntity - { - Id = hit.Id, - Name = hit.Name, - }); - kept++; + if (string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase)) + caseHits.Add(hit); } - StatusMessage = kept == 0 - ? "אין תוצאות מסוג תיק (Case)" - : $"נמצאו {kept} תיקים"; + + if (caseHits.Count == 0) + { + StatusMessage = "אין תוצאות מסוג תיק (Case)"; + return; + } + + // GlobalSearch only returns id+name. Hydrate each hit with + // number+status (and the Hebrew case name when present) so the + // picker shows the same columns the file-attachments picker + // does. + var detailTasks = new List>(caseHits.Count); + foreach (var hit in caseHits) + { + detailTasks.Add(LoadCaseRowAsync(hit, ct)); + } + var details = await Task.WhenAll(detailTasks).ConfigureAwait(true); + ct.ThrowIfCancellationRequested(); + + foreach (var row in details) + { + if (row != null) Cases.Add(row); + } + StatusMessage = $"נמצאו {Cases.Count} תיקים"; } catch (OperationCanceledException) { /* superseded */ } catch (EspoCrmAuthorizationException) @@ -101,6 +118,27 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels } } + private async Task LoadCaseRowAsync(EspoEntityRef hit, CancellationToken ct) + { + var row = new CaseEntity { Id = hit.Id, Name = hit.Name }; + if (string.IsNullOrEmpty(hit.Id)) return row; + try + { + var detail = await _client.GetCaseAsync( + hit.Id, "id,name,number,status,cCourtCaseNumber", ct).ConfigureAwait(false); + row.Number = detail.Number; + row.Status = detail.Status; + row.CourtCaseNumber = detail.CourtCaseNumber; + if (!string.IsNullOrEmpty(detail.Name)) row.Name = detail.Name; + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + _logger.Warning(ex, "ComposeFromCase: hydrating case {CaseId} failed", hit.Id); + } + return row; + } + [RelayCommand(CanExecute = nameof(CanSubmit))] private void Submit() { diff --git a/src/OutlookAddin/Services/ComposeService.cs b/src/OutlookAddin/Services/ComposeService.cs index cd635ba..080e1c1 100644 --- a/src/OutlookAddin/Services/ComposeService.cs +++ b/src/OutlookAddin/Services/ComposeService.cs @@ -42,21 +42,7 @@ namespace OutlookAddin.Services if (string.IsNullOrWhiteSpace(caseId)) throw new ArgumentException("caseId required", nameof(caseId)); var caseEntity = await _client.GetCaseAsync(caseId, "id,name,number,status,contactsIds,accountId").ConfigureAwait(true); - ContactEntity? primaryContact = null; - if (caseEntity.ContactsIds != null && caseEntity.ContactsIds.Count > 0) - { - try - { - primaryContact = await _client.GetContactAsync( - caseEntity.ContactsIds[0], - "id,name,emailAddress", - default).ConfigureAwait(true); - } - catch (Exception ex) - { - _logger.Warning(ex, "ComposeService: failed to load primary contact for case {CaseId}", caseId); - } - } + var recipientEmail = await ResolveRecipientEmailAsync(caseEntity, caseId).ConfigureAwait(true); var guid = Guid.NewGuid().ToString("N"); StampUserProperty(mail, EspoCaseIdProperty, caseEntity.Id); @@ -65,14 +51,14 @@ namespace OutlookAddin.Services try { - if (!string.IsNullOrWhiteSpace(primaryContact?.EmailAddress)) + if (!string.IsNullOrWhiteSpace(recipientEmail)) { var existingTo = mail.To ?? string.Empty; - if (!existingTo.Contains(primaryContact!.EmailAddress!)) + if (!existingTo.Contains(recipientEmail!)) { mail.To = string.IsNullOrEmpty(existingTo) - ? primaryContact.EmailAddress - : existingTo + "; " + primaryContact.EmailAddress; + ? recipientEmail + : existingTo + "; " + recipientEmail; } } @@ -107,6 +93,61 @@ namespace OutlookAddin.Services new FilingTarget("Case", caseEntity.Id, caseEntity.Name ?? caseEntity.Id)); } + /// + /// Find a usable recipient email for the case. EspoCRM's Case.contactsIds + /// is a many-to-many bag (primary + co-counsel + witnesses + …) and the + /// first entry isn't guaranteed to be the actual client — and even when + /// it is, the contact may have no emailAddress stored. So walk the list + /// and return the first non-empty address, then fall back to the + /// linked Account's email. + /// + private async Task ResolveRecipientEmailAsync(CaseEntity caseEntity, string caseId) + { + if (caseEntity.ContactsIds != null) + { + foreach (var contactId in caseEntity.ContactsIds) + { + if (string.IsNullOrWhiteSpace(contactId)) continue; + try + { + var contact = await _client.GetContactAsync( + contactId, + "id,name,emailAddress,emailAddressData", + default).ConfigureAwait(true); + if (!string.IsNullOrWhiteSpace(contact?.EmailAddress)) + { + return contact!.EmailAddress; + } + } + catch (Exception ex) + { + _logger.Warning(ex, "ComposeService: failed to load contact {ContactId} for case {CaseId}", contactId, caseId); + } + } + } + + if (!string.IsNullOrWhiteSpace(caseEntity.AccountId)) + { + try + { + var account = await _client.GetAccountAsync( + caseEntity.AccountId!, + "id,name,emailAddress", + default).ConfigureAwait(true); + if (!string.IsNullOrWhiteSpace(account?.EmailAddress)) + { + return account!.EmailAddress; + } + } + catch (Exception ex) + { + _logger.Warning(ex, "ComposeService: failed to load account {AccountId} for case {CaseId}", caseEntity.AccountId, caseId); + } + } + + return null; + } + public static string? ReadEspoCaseId(Outlook.MailItem mail) => TryReadUserProperty(mail, EspoCaseIdProperty); public static string? ReadEspoCaseName(Outlook.MailItem mail) => TryReadUserProperty(mail, EspoCaseNameProperty); public static string? ReadAddInGuid(Outlook.MailItem mail) => TryReadUserProperty(mail, AddInGuidProperty);