fix(compose-from-case): use any contact with email + show number/status
Walk Case.contactsIds for the first contact with a non-empty emailAddress (falling back to the Account's emailAddress) instead of blindly using contactsIds[0] and giving up if it has no email — Outlook was opening the new mail with no To recipient when the first linked contact was e.g. opposing counsel or a witness without a stored address. Also hydrate the "כתוב מתיק" picker rows with number/status/court# through GetCaseAsync (same pattern as SaveAttachmentsViewModel) and switch the dialog to the DataGrid layout so it matches the file- attachments picker the user expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<Window x:Class="MarcusLaw.OutlookAddin.UI.Dialogs.ComposeFromCaseDialog"
|
<Window x:Class="MarcusLaw.OutlookAddin.UI.Dialogs.ComposeFromCaseDialog"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:conv="clr-namespace:MarcusLaw.OutlookAddin.UI.Converters"
|
||||||
Title="כתוב מייל מתיק"
|
Title="כתוב מייל מתיק"
|
||||||
Height="480" Width="600"
|
Height="520" Width="680"
|
||||||
MinHeight="320" MinWidth="500"
|
MinHeight="360" MinWidth="540"
|
||||||
FlowDirection="RightToLeft"
|
FlowDirection="RightToLeft"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
ShowInTaskbar="False"
|
ShowInTaskbar="False"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
Background="#F8FAFC"
|
Background="#F8FAFC"
|
||||||
FontFamily="Segoe UI, David, Frank Ruehl, Arial">
|
FontFamily="Segoe UI, David, Frank Ruehl, Arial">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<conv:CaseStatusToHebrewConverter x:Key="CaseStatusHe" />
|
||||||
<SolidColorBrush x:Key="Primary" Color="#2563EB" />
|
<SolidColorBrush x:Key="Primary" Color="#2563EB" />
|
||||||
<SolidColorBrush x:Key="PrimaryHover" Color="#1D4ED8" />
|
<SolidColorBrush x:Key="PrimaryHover" Color="#1D4ED8" />
|
||||||
<SolidColorBrush x:Key="Border" Color="#E2E8F0" />
|
<SolidColorBrush x:Key="Border" Color="#E2E8F0" />
|
||||||
@@ -134,24 +136,30 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="1" Style="{StaticResource Card}">
|
<Border Grid.Row="1" Style="{StaticResource Card}">
|
||||||
<ListBox ItemsSource="{Binding Cases}"
|
<DataGrid ItemsSource="{Binding Cases}"
|
||||||
SelectedItem="{Binding SelectedCase}"
|
SelectedItem="{Binding SelectedCase}"
|
||||||
BorderThickness="0"
|
AutoGenerateColumns="False"
|
||||||
Background="Transparent"
|
HeadersVisibility="Column"
|
||||||
ItemContainerStyle="{StaticResource ResultItem}"
|
GridLinesVisibility="Horizontal"
|
||||||
Padding="4">
|
HorizontalGridLinesBrush="#E2E8F0"
|
||||||
<ListBox.ItemTemplate>
|
RowBackground="White"
|
||||||
<DataTemplate>
|
AlternatingRowBackground="#F8FAFC"
|
||||||
<StackPanel>
|
BorderThickness="0"
|
||||||
<TextBlock FontSize="13">
|
CanUserAddRows="False"
|
||||||
<Run Text="[" /><Run Text="{Binding Number, Mode=OneWay}" /><Run Text="] " />
|
CanUserDeleteRows="False"
|
||||||
<Run Text="{Binding Name, Mode=OneWay}" FontWeight="SemiBold" />
|
CanUserResizeRows="False"
|
||||||
</TextBlock>
|
CanUserReorderColumns="False"
|
||||||
<TextBlock Text="{Binding Status}" Foreground="{StaticResource Muted}" FontSize="11" />
|
SelectionMode="Single"
|
||||||
</StackPanel>
|
SelectionUnit="FullRow"
|
||||||
</DataTemplate>
|
IsReadOnly="True"
|
||||||
</ListBox.ItemTemplate>
|
RowHeight="30">
|
||||||
</ListBox>
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="מס׳ תיק" Binding="{Binding Number}" Width="80" />
|
||||||
|
<DataGridTextColumn Header="שם" Binding="{Binding Name}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="מס׳ בבית משפט" Binding="{Binding CourtCaseNumber}" Width="140" />
|
||||||
|
<DataGridTextColumn Header="סטטוס" Binding="{Binding Status, Converter={StaticResource CaseStatusHe}}" Width="110" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="{Binding StatusMessage}"
|
<TextBlock Grid.Row="2" Text="{Binding StatusMessage}"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -74,20 +75,36 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Cases.Clear();
|
Cases.Clear();
|
||||||
int kept = 0;
|
var caseHits = new List<EspoEntityRef>();
|
||||||
foreach (var hit in search.List)
|
foreach (var hit in search.List)
|
||||||
{
|
{
|
||||||
if (!string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase)) continue;
|
if (string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase))
|
||||||
Cases.Add(new CaseEntity
|
caseHits.Add(hit);
|
||||||
{
|
|
||||||
Id = hit.Id,
|
|
||||||
Name = hit.Name,
|
|
||||||
});
|
|
||||||
kept++;
|
|
||||||
}
|
}
|
||||||
StatusMessage = kept == 0
|
|
||||||
? "אין תוצאות מסוג תיק (Case)"
|
if (caseHits.Count == 0)
|
||||||
: $"נמצאו {kept} תיקים";
|
{
|
||||||
|
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<Task<CaseEntity?>>(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 (OperationCanceledException) { /* superseded */ }
|
||||||
catch (EspoCrmAuthorizationException)
|
catch (EspoCrmAuthorizationException)
|
||||||
@@ -101,6 +118,27 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<CaseEntity?> 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))]
|
[RelayCommand(CanExecute = nameof(CanSubmit))]
|
||||||
private void Submit()
|
private void Submit()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,21 +42,7 @@ namespace OutlookAddin.Services
|
|||||||
if (string.IsNullOrWhiteSpace(caseId)) throw new ArgumentException("caseId required", nameof(caseId));
|
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);
|
var caseEntity = await _client.GetCaseAsync(caseId, "id,name,number,status,contactsIds,accountId").ConfigureAwait(true);
|
||||||
ContactEntity? primaryContact = null;
|
var recipientEmail = await ResolveRecipientEmailAsync(caseEntity, caseId).ConfigureAwait(true);
|
||||||
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 guid = Guid.NewGuid().ToString("N");
|
var guid = Guid.NewGuid().ToString("N");
|
||||||
StampUserProperty(mail, EspoCaseIdProperty, caseEntity.Id);
|
StampUserProperty(mail, EspoCaseIdProperty, caseEntity.Id);
|
||||||
@@ -65,14 +51,14 @@ namespace OutlookAddin.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(primaryContact?.EmailAddress))
|
if (!string.IsNullOrWhiteSpace(recipientEmail))
|
||||||
{
|
{
|
||||||
var existingTo = mail.To ?? string.Empty;
|
var existingTo = mail.To ?? string.Empty;
|
||||||
if (!existingTo.Contains(primaryContact!.EmailAddress!))
|
if (!existingTo.Contains(recipientEmail!))
|
||||||
{
|
{
|
||||||
mail.To = string.IsNullOrEmpty(existingTo)
|
mail.To = string.IsNullOrEmpty(existingTo)
|
||||||
? primaryContact.EmailAddress
|
? recipientEmail
|
||||||
: existingTo + "; " + primaryContact.EmailAddress;
|
: existingTo + "; " + recipientEmail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +93,61 @@ namespace OutlookAddin.Services
|
|||||||
new FilingTarget("Case", caseEntity.Id, caseEntity.Name ?? caseEntity.Id));
|
new FilingTarget("Case", caseEntity.Id, caseEntity.Name ?? caseEntity.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string?> 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? ReadEspoCaseId(Outlook.MailItem mail) => TryReadUserProperty(mail, EspoCaseIdProperty);
|
||||||
public static string? ReadEspoCaseName(Outlook.MailItem mail) => TryReadUserProperty(mail, EspoCaseNameProperty);
|
public static string? ReadEspoCaseName(Outlook.MailItem mail) => TryReadUserProperty(mail, EspoCaseNameProperty);
|
||||||
public static string? ReadAddInGuid(Outlook.MailItem mail) => TryReadUserProperty(mail, AddInGuidProperty);
|
public static string? ReadAddInGuid(Outlook.MailItem mail) => TryReadUserProperty(mail, AddInGuidProperty);
|
||||||
|
|||||||
Reference in New Issue
Block a user