cfc2b393bd
- ReminderDto: базовый класс сузился до ReceiverEmail/ReceiverName/Url; RequestTypeId/RequestTypeName/RequestId/Status/RequestUniqueCode остались только в ApprovalOverdueReminderDto — ProbationEndingReminderDto они не нужны. Дискриминатор полиморфной сериализации ($type) больше не дублируется отдельным enum-полем Category. - ApprovalOverdueReminderDto: старый Email сохранён рядом с унаследованным ReceiverEmail (то же значение) — контракт можно переключать постепенно. - ApprovalRow/RowSelect: добавлен Title согласующего (TansuSiteApprover/Title) — заполняет ReceiverName для просроченных шагов, когда он есть в SharePoint. - EmployeeDirectorySnapshot.NameByEmail: обратный поиск ФИО руководителя по email — заполняет ReceiverName для напоминаний об испытательном сроке без лишнего похода в api.tnsu.kz (данные уже загружены для EmailOf). - GetProbationEndingAsync: испытательный срок не длиннее 6 месяцев, поэтому читаем только текущую и последнюю архивную партицию (не весь архив) и фильтруем строки по HireDate не старше 6 месяцев — экономим лишние запросы к SharePoint без потери актуальных заявок.
89 lines
5.3 KiB
C#
89 lines
5.3 KiB
C#
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||
|
||
namespace Tnsu.ApprovalRoute.Application.Reminders;
|
||
|
||
internal enum EmploymentStatus
|
||
{
|
||
/// <summary>Сотрудника с таким ФИО в справочнике нет вовсе — трудоустройство не подтверждено.</summary>
|
||
NotFound,
|
||
|
||
/// <summary>Работает хотя бы в одной компании.</summary>
|
||
Employed,
|
||
|
||
/// <summary>Есть в справочнике, но уволен из всех компаний, где числился.</summary>
|
||
Dismissed,
|
||
}
|
||
|
||
/// <summary>
|
||
/// Оба справочника api.tnsu.kz разом, разобранные по ФИО: дата увольнения есть
|
||
/// только в /Employee/FullBase, а email — только в /Employee/All, поэтому нужны
|
||
/// оба. Ищем ПО ФИО, потому что точечного поиска по нему у api.tnsu.kz нет
|
||
/// (эндпоинта /Employee/ByFio не существует), а в заявке на приём кандидат
|
||
/// записан именно ФИО (колонка CandidateName) — временное решение, см.
|
||
/// <see cref="ITnsuPortalClient.GetAllEmployeesAsync"/>.
|
||
///
|
||
/// ФИО не уникально: один человек может числиться в нескольких компаниях (тогда
|
||
/// это несколько записей одного и того же человека), но с тем же успехом могут
|
||
/// совпасть ФИО двух разных людей — различить эти два случая по одному ФИО нельзя,
|
||
/// поэтому все совпадения трактуются как записи одного человека.
|
||
/// </summary>
|
||
internal sealed class EmployeeDirectorySnapshot
|
||
{
|
||
private readonly ILookup<string, EmployeeFullBaseInfo> _fullBaseByFio;
|
||
private readonly ILookup<string, EmployeeContactInfo> _contactsByFio;
|
||
private readonly IReadOnlyDictionary<string, string> _fioByEmail;
|
||
|
||
public EmployeeDirectorySnapshot(
|
||
IReadOnlyList<EmployeeFullBaseInfo> fullBase, IReadOnlyList<EmployeeContactInfo> contacts)
|
||
{
|
||
_fullBaseByFio = fullBase.ToLookup(e => Normalize(e.Fio));
|
||
_contactsByFio = contacts.ToLookup(e => Normalize(e.Fio));
|
||
|
||
// Обратный индекс email → ФИО — нужен, чтобы подписать напоминание именем
|
||
// получателя, когда известен только его email (руководитель кандидата,
|
||
// см. GetManagerAsync). При дублях email в справочнике побеждает первая
|
||
// запись — этого достаточно для отображения имени, не для идентификации.
|
||
_fioByEmail = contacts
|
||
.Where(e => !string.IsNullOrWhiteSpace(e.Email))
|
||
.GroupBy(e => e.Email!.Trim().ToLowerInvariant())
|
||
.ToDictionary(g => g.Key, g => g.First().Fio);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Уволенным считаем только того, у кого дата увольнения стоит во ВСЕХ его
|
||
/// записях: сотрудник, уволенный из одной компании холдинга, но оставшийся в
|
||
/// другой, продолжает работать — и напоминание ему по-прежнему нужно.
|
||
/// </summary>
|
||
public EmploymentStatus EmploymentOf(string fio)
|
||
{
|
||
var entries = _fullBaseByFio[Normalize(fio)].ToList();
|
||
if (entries.Count == 0) return EmploymentStatus.NotFound;
|
||
|
||
return entries.All(e => e.DismissalDate is not null)
|
||
? EmploymentStatus.Dismissed
|
||
: EmploymentStatus.Employed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Первый непустой email среди записей этого ФИО. У одного человека записей
|
||
/// несколько (по компании на каждую), а email у него один и тот же — но
|
||
/// заполнен он может быть не в каждой записи.
|
||
/// </summary>
|
||
public string? EmailOf(string fio) =>
|
||
_contactsByFio[Normalize(fio)]
|
||
.Select(e => e.Email)
|
||
.FirstOrDefault(email => !string.IsNullOrWhiteSpace(email));
|
||
|
||
/// <summary>ФИО по email — для ReceiverName, когда известен только email получателя (см. EmailOf).</summary>
|
||
public string? NameByEmail(string email) =>
|
||
_fioByEmail.GetValueOrDefault(email.Trim().ToLowerInvariant());
|
||
|
||
/// <summary>
|
||
/// В справочнике и в заявке ФИО набирают руками, поэтому сравниваем без учёта
|
||
/// регистра и лишних пробелов между словами (в живых данных встречаются и
|
||
/// двойные пробелы, и неразрывный пробел — см. поле department в /Employee/All).
|
||
/// </summary>
|
||
private static string Normalize(string fio) =>
|
||
string.Join(' ', fio.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).ToLowerInvariant();
|
||
}
|