From 722decd040b5f470adf3d5f7e4aa76069c6387e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:57:02 +0300 Subject: [PATCH] Manuel Notification eklendi --- .../lowcode-reference.instructions.md | 2 + README.md | 40 ++ .../NotificationConsts.cs | 19 + .../Entities/Notification.cs | 7 + .../NotificationManager.cs | 148 +++++- ...icationDbContextModelCreatingExtensions.cs | 1 + .../CreateNotificationManualInput.cs | 23 + .../IListFormDynamicApiAppService.cs | 1 + .../LookUpQueryValues.cs | 32 ++ .../ListForms/ListFormDynamicApiAppService.cs | 101 +++- .../Migrations/LanguagesData.json | 126 +++++ .../ListFormSeeder_Administration.cs | 481 ++++++++++++++++++ .../Migrations/ListFormSeeder_DefaultJsons.cs | 23 + .../Migrations/ListFormSeeder_Saas.cs | 51 +- .../Enums/TableNameEnum.cs | 2 + .../PlatformConsts.cs | 13 + .../TableNameResolver.cs | 2 + .../BackgroundWorkers/NotificationWorker.cs | 7 +- .../Data/SeedConsts.cs | 2 + .../Administration/NotificationManual.cs | 36 ++ .../Administration/NotificationTemplate.cs | 30 ++ .../EntityFrameworkCore/PlatformDbContext.cs | 38 ++ ....cs => 20260921060400_Initial.Designer.cs} | 165 +++++- ...9_Initial.cs => 20260921060400_Initial.cs} | 72 +++ .../PlatformDbContextModelSnapshot.cs | 163 ++++++ .../host/data/Abp.Identity.Permissions.json | 134 ++++- configs/seeds/host/data/App.Menus.Menu.json | 49 +- .../App.Notifications.NotificationRules.json | 10 + ...p.Notifications.NotificationTemplates.json | 44 ++ .../App.Notifications.NotificationTypes.json | 5 + 30 files changed, 1772 insertions(+), 55 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/CreateNotificationManualInput.cs create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationManual.cs create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationTemplate.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260920201729_Initial.Designer.cs => 20260921060400_Initial.Designer.cs} (98%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260920201729_Initial.cs => 20260921060400_Initial.cs} (98%) create mode 100644 configs/seeds/host/data/App.Notifications.NotificationTemplates.json diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 4a33682e..3a878ff2 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -1213,6 +1213,8 @@ Kök menü `App.Administration`. Bu menünün tabloları `Adm_T_*` — yani **ki | **Licence** | `App.Licence` · `/admin/form/App.Licence` | `AbpTenants` + `V_Sas_H_OrderItem_ByTenantId` | **`ListFormType = Form`** (listesiz, tek kayıtlık ekran) + alt kalem listesi. "Tek kayıtlık kart ekran" isteğinin karşılığı budur. | | **Dev Request** | `App.DevRequest` | `Sas_H_DevRequest` | Kiracı tarafındaki talep ekranı (Saas'taki listenin kullanıcı yüzü). | | **Agenda** | `App.Agenda` | `Adm_T_Agenda` | **Takvim (Scheduler) düzeninin** referansı: `StartDate`, `EndDate`, `AllDay`, `RecurrenceRule`, `RecurrenceException`. Tekrarlayan randevu isteği kod gerektirmez. | +| **Bildirimler › Manuel Bildirim** | `App.ManualNotification` | `Adm_T_NotificationManual` | Seçilen kullanıcılara seçilen kanallardan elle bildirim gönderimi; kayıt gönderimin günlüğüdür, gönderim `INotificationManager` üzerinden "Manuel" tipiyle yapılır. Ayrıntı: `README.md` §13.5.1. | +| **Bildirimler › Bildirim Şablonları** | `App.Notifications.NotificationTemplates` | `Adm_T_NotificationTemplate` | Manuel gönderimde seçilen mesaj şablonları (kanal, konu, gövde, yer tutucular); şablon seçilince konu ve gövde forma kopyalanır. Örnek satırlar seed ile gelir. | | **Settings** | `App.Setting` · `/admin/settings` | — | `SettingDefinition` kayıtlarından üretilen ayar ekranı; değerler burada girilir. | | **Intranet › Announcement (+Like/Comment)** | `App.Intranet.Announcement` | `Adm_T_Announcement` · `Adm_T_AnnouncementLike` · `Adm_T_AnnouncementComment` | Duyuru + beğeni + yorum: **ana kayıt / alt etkileşim** kalıbının hazır örneği. | | **Intranet › Survey (+Question/Option/Response/Answer)** | `App.Intranet.Survey` | `Adm_T_Survey` · `Adm_T_SurveyQuestion` · `Adm_T_SurveyQuestionOption` · `Adm_T_SurveyResponse` · `Adm_T_SurveyAnswer` | Anket motoru; soru–seçenek–yanıt zinciri **iki seviyeli alt form** ihtiyacının referansıdır. | diff --git a/README.md b/README.md index d3cc77ae..b46e1a1f 100644 --- a/README.md +++ b/README.md @@ -1196,6 +1196,46 @@ Tercih ekranı (`/admin/profile/notification-settings`) satırlarını bildirim üzerinden eşler; tip adı yalnızca gösterimdir. `NotificationType.Description` alanı aynı ekranda ipucu olarak görünür ve Bildirim Türleri ekranından yazılır. +### 13.5.1. Manuel gönderim (Administration › Bildirimler) + +İki ekran Administration menüsünde `App.Administration.Notifications` grubunda durur: +**Manuel Bildirim** ve **Bildirim Şablonları**. Grup Agenda'nın üstündedir; Saas › Notifications +altında bildirim **tipi/kuralı/kaydı** ekranları kalır. + +Kural yazmadan, seçilen kullanıcılara seçilen kanallardan bildirim göndermek için +`App.ManualNotification` ekranı kullanılır (`Adm_T_NotificationManual`). Kayıt gönderimin kendisi +değil **günlüğüdür**: ekleme anında yazma işi `list-form-dynamic-api/notification-manual-insert` +ucuna gider (`InsertServiceAddress`), uç kaydı yazar ve gönderimi +`INotificationManager.SendManualNotificationAsync` üzerinden yapar — yani Bildirim Kuralları +ekranındaki elle gönderimle **aynı yol**, tek fark alıcı kümesinin kurallardan değil ekrandan +gelmesi. Güncelleme kapalıdır, silme kaydı kaldırır. + +| Kural | Nerede | +| --- | --- | +| Bildirim tipi her zaman `Manuel` | `NotificationConsts.ManualTypeName`; tip kaydı `App.Notifications.NotificationTypes.json` | +| `Notification.NotificationRuleId` NOT NULL olduğu için tipin **çapa kuralına** bağlanır (`RecipientType = Custom`, `IsActive = false`) | `App.Notifications.NotificationRules.json`; kural pasif olduğu için kural motoru ve profil tercih ekranında görünmez | +| Kullanıcı tercihi okunmaz (zorunlu bildirim gibi davranır) | `NotificationManager.SendManualNotificationAsync` | +| Alıcı listesi kiracı filtresinden geçer | Kullanıcı sorgusu ABP kiracı filtresiyle çalışır; istekteki kimlikler değiştirilse bile başka kiracıya gönderilemez | +| İlgili kanalda adresi olmayan alıcı atlanır | `SkippedCount` + `ResultNote` (kanal bazlı gerekçe) | +| Kanaldan çıkış yine `NotificationWorker` | `UiActivity` oluşurken iletilmiş sayılır, tarayıcı kanallarını istemci işaretler (§13.3) | + +**Şablonlar** (`Administration › Bildirimler › Bildirim Şablonları`, +`App.Notifications.NotificationTemplates`, +`Adm_T_NotificationTemplate`) mesaj metnini taşır: `Code`, `Name`, `Channel` (boş = tüm kanallar), +`Subject`, `Body`, `IsActive`. Örnek şablonlar seed ile gelir +(`data/App.Notifications.NotificationTemplates.json`; ekran `SeedFilePath` ile bu dosyaya +aynalanır). Gönderimde şablon seçilirse ekranda **boş bırakılan** konu ve gövde şablondan +tamamlanır; ekranda yazılan metin şablonu ezer. + +Gövdedeki yer tutucular alıcı bazında çözülür (`NotificationMessageTokens`): `{AdSoyad}`, +`{KullaniciAdi}`, `{Eposta}`, `{Tarih}`, `{Saat}`, `{Kiraci}`. Bildirim kaydının `Message` kolonu +2048 karakterdir; yer tutucular çözülünce metin bu sınırda kırpılır. + +**Mail konu satırı:** bildirim kaydı `Subject` kolonu taşır (`Sas_H_Notification`, 256). +`NotificationWorker` konuyu bu kolondan alır; kolon boşsa bildirim tipinin adına düşer +(`Platform Bildirim: {tip}`). Kural motorundan gelen bildirimlerde kolon boş kalır, manuel +gönderimde ekrandan ya da şablondan gelir — yani konu artık gövdeye gömülmez. + ### 13.6. Göndericiler (`Sozsoft.Sender`) | Kanal | Sağlayıcı | diff --git a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/NotificationConsts.cs b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/NotificationConsts.cs index 8b7004db..161bc2f3 100644 --- a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/NotificationConsts.cs +++ b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/NotificationConsts.cs @@ -3,6 +3,12 @@ public static class NotificationConsts { public const string AppName = "Notifications"; + + /// + /// Elle yapilan gonderimlerin bildirim tipi. Kural motorundan gecmeyen her bildirim bu tiple + /// yazilir; tip kaydi seed ile gelir (App.Notifications.NotificationTypes.json). + /// + public const string ManualTypeName = "Manuel"; } public static class NotificationRecipientTypes @@ -14,3 +20,16 @@ public static class NotificationRecipientTypes public const string Custom = "Custom"; } +/// +/// Manuel gonderimde mesaj icinde cozulen yer tutucular. Sablon metni bu anahtarlari tasir, +/// gonderim aninda her alici icin ayri ayri degistirilir. +/// +public static class NotificationMessageTokens +{ + public const string FullName = "{AdSoyad}"; + public const string UserName = "{KullaniciAdi}"; + public const string Email = "{Eposta}"; + public const string Date = "{Tarih}"; + public const string Time = "{Saat}"; + public const string Tenant = "{Kiraci}"; +} diff --git a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Entities/Notification.cs b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Entities/Notification.cs index ea64ffda..656ceb5e 100644 --- a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Entities/Notification.cs +++ b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Entities/Notification.cs @@ -15,6 +15,13 @@ public class Notification : FullAuditedEntity public Guid? UserId { get; set; } public required string Message { get; set; } + /// + /// Mail kanalinin konu satiri. Bos ise NotificationWorker konuyu bildirim tipinin + /// adindan uretir; kural motorundan gecen bildirimlerde bos kalir, manuel gonderimde + /// ekrandan ya da sablondan gelir. + /// + public string? Subject { get; set; } + public bool IsSent { get; set; } public bool IsRead { get; set; } public DateTime? ReadTime { get; set; } diff --git a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/NotificationManager.cs b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/NotificationManager.cs index ef6aea97..09ca0d2a 100644 --- a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/NotificationManager.cs +++ b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/NotificationManager.cs @@ -5,9 +5,12 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Sozsoft.Notifications.Entities; using Sozsoft.Notifications.Enums; +using Volo.Abp; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; namespace Sozsoft.Notifications.Domain; @@ -18,6 +21,36 @@ public interface INotificationManager : ITransientDependency /// kaydi uretir. Kayitlarin kanaldan cikisi NotificationWorker'a aittir. /// Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage); + + /// + /// Alici kumesi kurallardan degil ekrandan gelen gonderim: secilen her kullanici icin secilen + /// her kanalda bir bildirim kaydi uretir. Kanaldan cikis yine NotificationWorker'a + /// aittir; kullanici tercihi dikkate alinmaz, cunku gonderim zaten bilincli bir aksiyondur. + /// + Task SendManualNotificationAsync(ManualNotificationRequest request); +} + +/// Manuel gonderim istegi; alicilar ve kanallar cagirandan gelir. +public class ManualNotificationRequest +{ + public Guid NotificationTypeId { get; set; } + public IReadOnlyCollection UserIds { get; set; } = []; + public IReadOnlyCollection Channels { get; set; } = []; + public string Message { get; set; } = string.Empty; + + /// Mail konu satiri; bos ise konu bildirim tipinin adindan uretilir. + public string Subject { get; set; } +} + +/// +/// Manuel gonderimin sonucu. Atlananlar kanal kanal raporlanir: bir alicinin o kanalda adresi +/// (e-posta, telefon, chat id) yoksa bildirim uretilemez ve bunun cozumu kullanici kaydindadir. +/// +public class ManualNotificationResult +{ + public int SentCount { get; set; } + public Dictionary SkippedByChannel { get; set; } = []; + public int SkippedCount => SkippedByChannel.Values.Sum(); } public class NotificationManager : INotificationManager @@ -29,6 +62,8 @@ public class NotificationManager : INotificationManager private readonly INotificationIdentifierProvider notificationIdentifierProvider; private readonly IOrganizationUnitRepository repositoryOrganizationUnit; private readonly INotificationRealtimeNotifier realtimeNotifier; + private readonly ICurrentTenant currentTenant; + private readonly IClock clock; public NotificationManager( IReadOnlyRepository repository, @@ -37,7 +72,9 @@ public class NotificationManager : INotificationManager IRepository repositoryNotification, INotificationIdentifierProvider notificationIdentifierProvider, IOrganizationUnitRepository repositoryOrganizationUnit, - INotificationRealtimeNotifier realtimeNotifier + INotificationRealtimeNotifier realtimeNotifier, + ICurrentTenant currentTenant, + IClock clock ) { this.repository = repository; @@ -47,6 +84,8 @@ public class NotificationManager : INotificationManager this.notificationIdentifierProvider = notificationIdentifierProvider; this.repositoryOrganizationUnit = repositoryOrganizationUnit; this.realtimeNotifier = realtimeNotifier; + this.currentTenant = currentTenant; + this.clock = clock; } public async Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage) @@ -121,16 +160,119 @@ public class NotificationManager : INotificationManager return; } + await SaveAndAnnounceAsync(notifications); + } + + /// + /// Alicilari kurallar yerine cagiranin verdigi gonderim. Kullanici tercihi okunmaz: manuel + /// gonderim zorunlu bildirim (IsFixed) gibi davranir, aksi halde yonetici bilincli + /// yaptigi gonderimin kime gitmedigini hic ogrenemezdi. + /// + public async Task SendManualNotificationAsync(ManualNotificationRequest request) + { + var result = new ManualNotificationResult(); + + var channels = (request.Channels ?? []) + .Where(channel => !channel.IsNullOrWhiteSpace()) + .Select(channel => channel.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + var userIds = (request.UserIds ?? []).Distinct().ToList(); + + if (channels.Count == 0 || userIds.Count == 0 || request.Message.IsNullOrWhiteSpace()) + { + return result; + } + + // Sas_H_Notification.NotificationRuleId NOT NULL'dur. Manuel gonderim kural motorundan + // gecmedigi icin tipin capa kuralina baglanir; kural pasiftir, alici kumesini o belirlemez. + var anchorRule = await repository.FirstOrDefaultAsync(rule => + rule.NotificationTypeId == request.NotificationTypeId + && rule.RecipientType == NotificationRecipientTypes.Custom) + ?? await repository.FirstOrDefaultAsync(rule => rule.NotificationTypeId == request.NotificationTypeId) + ?? throw new BusinessException("Notifications:ManualAnchorRuleNotFound") + .WithData("NotificationTypeId", request.NotificationTypeId); + + // Kullanici sorgusu ABP kiraci filtresinden gecer; istekteki kimlikler degistirilse bile + // baska bir kiracinin kullanicisina bildirim uretilemez. + var queryUsers = await repositoryUser.GetQueryableAsync(); + var users = await queryUsers + .Where(user => userIds.Contains(user.Id) && user.IsActive) + .Select(user => new ManualRecipient(user.Id, user.UserName, user.Name, user.Surname, user.Email)) + .ToListAsync(); + + var notifications = new List(); + foreach (var channel in channels) + { + foreach (var user in users) + { + var identifier = await notificationIdentifierProvider.GetIdentifier(user.Id, channel); + if (identifier.IsNullOrWhiteSpace()) + { + result.SkippedByChannel[channel] = result.SkippedByChannel.GetOrDefault(channel) + 1; + continue; + } + + notifications.Add(new Notification + { + NotificationTypeId = request.NotificationTypeId, + NotificationRuleId = anchorRule.Id, + NotificationChannel = channel, + Identifier = identifier!, + UserId = user.Id, + Message = ResolveTokens(request.Message, user), + Subject = request.Subject, + IsSent = IsDeliveredOnCreate(channel), + IsRead = false + }); + } + } + + result.SentCount = notifications.Count; + await SaveAndAnnounceAsync(notifications); + + return result; + } + + /// Mesajdaki yer tutucular alici bazinda cozulur; her alici kendi metnini alir. + private string ResolveTokens(string message, ManualRecipient recipient) + { + var now = clock.Now; + var resolved = message + .Replace(NotificationMessageTokens.FullName, $"{recipient.Name} {recipient.Surname}".Trim()) + .Replace(NotificationMessageTokens.UserName, recipient.UserName ?? string.Empty) + .Replace(NotificationMessageTokens.Email, recipient.Email ?? string.Empty) + .Replace(NotificationMessageTokens.Date, now.ToShortDateString()) + .Replace(NotificationMessageTokens.Time, now.ToShortTimeString()) + .Replace(NotificationMessageTokens.Tenant, currentTenant.Name ?? string.Empty); + + // Yer tutucular cozulunce metin uzayabilir; kayit kolonu 2048 karakterdir. + return resolved.Length > 2048 ? resolved[..2048] : resolved; + } + + /// + /// Kayitlari yazar ve acik istemcilere duyurur. Duyuru kayit yazildiktan sonra yapilir; + /// istemci duyuruyu alip listesini okudugunda kaydi mutlaka gormeli. Duyuru gitmezse + /// istemcideki yedek yoklama ayni kaydi getirir. + /// + private async Task SaveAndAnnounceAsync(List notifications) + { + if (notifications.Count == 0) + { + return; + } + await repositoryNotification.InsertManyAsync(notifications, autoSave: true); - // Duyuru kayit yazildiktan sonra yapilir; istemci duyuruyu alip listesini okudugunda - // kaydi mutlaka gormeli. Duyuru gitmezse yedek yoklama ayni kaydi getirir. await realtimeNotifier.NotifyAsync( [.. notifications .Where(notification => notification.UserId.HasValue) .Select(notification => new NotificationDelivery(notification.UserId!.Value, notification.NotificationChannel))]); } + /// Yer tutucularin cozulmesi icin gereken alici bilgisi. + private sealed record ManualRecipient(Guid Id, string UserName, string Name, string Surname, string Email); + /// Kuralin hedefledigi kullanicilar; Custom alici bu yoldan gecmez. private async Task> ResolveRecipientUserIdsAsync(NotificationRule notificationRule, IQueryable queryUsers) { diff --git a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationDbContextModelCreatingExtensions.cs b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationDbContextModelCreatingExtensions.cs index 80775e27..7b01192a 100644 --- a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationDbContextModelCreatingExtensions.cs +++ b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationDbContextModelCreatingExtensions.cs @@ -51,6 +51,7 @@ public static class NotificationDbContextModelCreatingExtensions b.Property(x => x.NotificationChannel).IsRequired().HasMaxLength(64); b.Property(x => x.Identifier).IsRequired().HasMaxLength(256); b.Property(x => x.Message).IsRequired().HasMaxLength(2048); + b.Property(x => x.Subject).HasMaxLength(256); b.HasOne(x => x.NotificationType) .WithMany(x => x.Notifications) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/CreateNotificationManualInput.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/CreateNotificationManualInput.cs new file mode 100644 index 00000000..18d6ec47 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/CreateNotificationManualInput.cs @@ -0,0 +1,23 @@ +using System; +using System.Text.Json.Serialization; + +namespace Sozsoft.Platform.ListForms.DynamicApi; + +/// +/// Manuel bildirim gonderim ekraninin kayit girdisi. ve +/// ekranda dxTagBox ile secilir; tipli DTO yolunda +/// QueryHelper devreye girmedigi icin dizi/metin donusumu converter ile yapilir. +/// +public class CreateNotificationManualInput +{ + public string NotificationTemplateId { get; set; } + + [JsonConverter(typeof(MultiValueStringJsonConverter))] + public string Channels { get; set; } + + [JsonConverter(typeof(MultiValueStringJsonConverter))] + public string UserIds { get; set; } + + public string Subject { get; set; } + public string Message { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/IListFormDynamicApiAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/IListFormDynamicApiAppService.cs index f13ada23..14b4d871 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/IListFormDynamicApiAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/DynamicApi/IListFormDynamicApiAppService.cs @@ -13,4 +13,5 @@ public interface IListFormDynamicApiAppService Task PostTenantUpdateAsync(DynamicApiBaseInput input); Task PostTenantDeleteAsync(DynamicApiBaseInput input); Task PostCreateTenantFromOrderAsync(DynamicApiBaseInput input); + Task PostNotificationManualInsertAsync(DynamicApiBaseInput input); } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/LookUpQueryValues.cs b/api/src/Sozsoft.Platform.Application.Contracts/LookUpQueryValues.cs index 56cffdd1..7eacceaf 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/LookUpQueryValues.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/LookUpQueryValues.cs @@ -146,6 +146,38 @@ public static class LookupQueryValues $"FROM \"AbpRoles\"" + $"ORDER BY \"Name\""; + /// + /// Manuel bildirim alicilari: yalnizca oturumun kiracisindaki aktif kullanicilar. Kiraci + /// filtresi '@TENANTID' ile kurulur, host baglaminda "IS NULL" olarak cozulur. + /// + public static string TenantUserValues = + $"SELECT " + + $"\"Id\" AS \"Key\", " + + $"CONCAT(\"Name\", ' ', \"Surname\", ' (', \"UserName\", ')') AS \"Name\" " + + $"FROM \"AbpUsers\" " + + $"WHERE " + + $"\"IsDeleted\" = 'false' " + + $"AND \"IsActive\" = 'true' " + + $"AND \"TenantId\" = '@TENANTID' " + + $"ORDER BY \"Name\";"; + + /// + /// Manuel gonderimde secilebilen aktif sablonlar. Konu ve govde de secilir: ekrandaki + /// copy kurali sablon secilince bu kolonlari forma tasir (EditorScript). + /// + public static string NotificationTemplateValues = + $"SELECT " + + $"\"Id\" AS \"Key\", " + + $"\"Name\" AS \"Name\", " + + $"\"Subject\", " + + $"\"Body\" " + + $"FROM \"{FullNameTable(TableNameEnum.NotificationTemplate)}\" " + + $"WHERE " + + $"\"IsDeleted\" = 'false' " + + $"AND \"IsActive\" = 'true' " + + $"AND \"TenantId\" = '@TENANTID' " + + $"ORDER BY \"Name\";"; + public static string UserValues = $"SELECT " + $"\"Id\" AS \"Key\", " + diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs index e900655f..664b9861 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Sozsoft.Notifications.Domain; using Sozsoft.Platform.BlobStoring; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Extensions; @@ -36,6 +38,10 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic private readonly IOptions identityOptions; private readonly IRepository orderRepository; private readonly IListFormSeedDataSynchronizer seedDataSynchronizer; + private readonly IRepository notificationManualRepository; + private readonly IRepository notificationTemplateRepository; + private readonly IReadOnlyRepository notificationTypeRepository; + private readonly INotificationManager notificationManager; public ListFormDynamicApiAppService( ITenantRepository tenantRepository, @@ -46,7 +52,11 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic IConfiguration configuration, IOptions identityOptions, IRepository orderRepository, - IListFormSeedDataSynchronizer seedDataSynchronizer) + IListFormSeedDataSynchronizer seedDataSynchronizer, + IRepository notificationManualRepository, + IRepository notificationTemplateRepository, + IReadOnlyRepository notificationTypeRepository, + INotificationManager notificationManager) { this.tenantRepository = tenantRepository; this.tenantManager = tenantManager; @@ -57,6 +67,10 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic this.identityOptions = identityOptions; this.orderRepository = orderRepository; this.seedDataSynchronizer = seedDataSynchronizer; + this.notificationManualRepository = notificationManualRepository; + this.notificationTemplateRepository = notificationTemplateRepository; + this.notificationTypeRepository = notificationTypeRepository; + this.notificationManager = notificationManager; } private static Guid ParseGuid(string value) @@ -382,5 +396,88 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic await seedDataSynchronizer.RemoveRowsAsync(input.ListFormCode, input.Keys); } -} + /// + /// Manuel bildirim gonderimi. Kayit olusur olusmaz gonderim yapilir: kayit gonderimin kendisi + /// degil gunlugudur. Gonderim yolu Bildirim Kurallari ekranindaki elle gonderimle aynidir + /// (INotificationManager); tek fark alici kumesinin kurallardan degil ekrandan gelmesi. + /// + [Authorize(PlatformConsts.AppCodes.ManualNotifications.Create)] + public async Task PostNotificationManualInsertAsync(DynamicApiBaseInput input) + { + var data = input.Data ?? throw new UserFriendlyException(L["App.Platform.Error"]); + if (data.Channels.IsNullOrWhiteSpace() || data.UserIds.IsNullOrWhiteSpace()) + { + throw new UserFriendlyException(L["App.ManualNotification.RecipientAndChannelRequired"]); + } + + var templateId = ParseGuid(data.NotificationTemplateId); + var subject = data.Subject; + var message = data.Message; + + // Sablon secildiginde ekranda bos birakilan konu ve govde sablondan tamamlanir; ekranda + // yazilan metin sablonu ezer, cunku gonderen son sozu soyler. + if (templateId != Guid.Empty) + { + var template = await notificationTemplateRepository.GetAsync(templateId); + subject = subject.IsNullOrWhiteSpace() ? template.Subject : subject; + message = message.IsNullOrWhiteSpace() ? template.Body : message; + } + + if (message.IsNullOrWhiteSpace()) + { + throw new UserFriendlyException(L["App.ManualNotification.MessageRequired"]); + } + + var manualType = await notificationTypeRepository.FirstOrDefaultAsync(type => type.Name == NotificationConsts.ManualTypeName) + ?? throw new UserFriendlyException(L["App.ManualNotification.TypeNotFound"]); + + var record = await notificationManualRepository.InsertAsync( + new NotificationManual + { + TenantId = CurrentTenant.Id, + NotificationTemplateId = templateId == Guid.Empty ? null : templateId, + Channels = data.Channels, + UserIds = data.UserIds, + Subject = subject, + Message = message, + }, + autoSave: true); + + var result = await notificationManager.SendManualNotificationAsync(new ManualNotificationRequest + { + NotificationTypeId = manualType.Id, + Channels = SplitMultiValue(data.Channels), + UserIds = [.. SplitMultiValue(data.UserIds).Select(ParseGuid).Where(id => id != Guid.Empty)], + Message = message, + Subject = subject, + }); + + record.SentTime = Clock.Now; + record.SentCount = result.SentCount; + record.SkippedCount = result.SkippedCount; + record.ResultNote = BuildSkippedNote(result); + await notificationManualRepository.UpdateAsync(record, autoSave: true); + + return record.Id; + } + + private static List SplitMultiValue(string value) + { + return [.. (value ?? string.Empty) + .Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; + } + + /// + /// Atlananlarin kanal bazli gerekcesi. Adresi olmayan alici gonderimi durdurmaz; gonderen + /// kimin alamadigini kayitta gorur, cozum kullanici kaydindadir. + /// + private string BuildSkippedNote(ManualNotificationResult result) + { + return result.SkippedCount == 0 + ? null + : string.Join(" | ", result.SkippedByChannel + .Where(item => item.Value > 0) + .Select(item => L["App.ManualNotification.SkippedChannel", item.Key, item.Value].Value)); + } +} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 676f256c..de7edf5b 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -28769,6 +28769,132 @@ "key": "App.Mcp.Error.RegistrationLimitReached", "en": "The limit of {0} dynamically registered MCP clients has been reached. Remove unused clients from the OpenIddict applications.", "tr": "Dinamik kayıtlı MCP istemcisi tavanına ({0}) ulaşıldı. Kullanılmayan istemcileri OpenIddict uygulamalarından silin." + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification.RecipientAndChannelRequired", + "en": "Select at least one recipient and one channel.", + "tr": "En az bir alıcı ve bir kanal seçin." + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification.MessageRequired", + "en": "The message is empty: type a body or choose a template that has one.", + "tr": "Mesaj boş: bir gövde yazın ya da gövdesi olan bir şablon seçin." + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification.TypeNotFound", + "en": "The \"Manuel\" notification type was not found; run the seed data.", + "tr": "\"Manuel\" bildirim tipi bulunamadı; seed verisini çalıştırın." + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification.SkippedChannel", + "en": "{0}: {1} recipient(s) have no address", + "tr": "{0}: {1} alıcının adresi yok" + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification", + "en": "Manual Notification", + "tr": "Manuel Bildirim" + }, + { + "resourceName": "Platform", + "key": "App.ManualNotification.Description", + "en": "Sends a notification to the selected users over the selected channels; every send is queued with the \"Manuel\" notification type.", + "tr": "Seçilen kullanıcılara seçilen kanallardan bildirim gönderir; her gönderim \"Manuel\" bildirim tipiyle kuyruğa yazılır." + }, + { + "resourceName": "Platform", + "key": "App.Notifications.NotificationTemplates", + "en": "Notification Templates", + "tr": "Bildirim Şablonları" + }, + { + "resourceName": "Platform", + "key": "App.Notifications.NotificationTemplates.Description", + "en": "Reusable message templates for manual notifications. Placeholders resolved per recipient: {AdSoyad}, {KullaniciAdi}, {Eposta}, {Tarih}, {Saat}, {Kiraci}.", + "tr": "Manuel bildirim gönderiminde kullanılan mesaj şablonları. Alıcı bazında çözülen yer tutucular: {AdSoyad}, {KullaniciAdi}, {Eposta}, {Tarih}, {Saat}, {Kiraci}." + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.Body", + "en": "Body", + "tr": "Gövde" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.Channels", + "en": "Channels", + "tr": "Kanallar" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.UserIds", + "en": "Recipients", + "tr": "Alıcılar" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.NotificationTemplateId", + "en": "Template", + "tr": "Şablon" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.SentTime", + "en": "Sent Time", + "tr": "Gönderim Zamanı" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.SentCount", + "en": "Sent", + "tr": "Gönderilen" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.SkippedCount", + "en": "Skipped", + "tr": "Atlanan" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.ResultNote", + "en": "Result", + "tr": "Sonuç" + }, + { + "resourceName": "Platform", + "key": "App.Administration.Notifications", + "en": "Notifications", + "tr": "Bildirimler" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.Event", + "en": "Event", + "tr": "Olay" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.QuestionId", + "en": "Question", + "tr": "Soru" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.ResponseId", + "en": "Response", + "tr": "Yanıt" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.OrderId", + "en": "Order", + "tr": "Sipariş" } ] } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs index fe40256b..5d962110 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs @@ -6874,6 +6874,487 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep } #endregion + #region Notification Template + listFormName = AppCodes.Notifications.NotificationTemplates; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = $"{listFormName}.Description", + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationTemplate)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson(), + HeaderFilterJson = DefaultHeaderFilterJson(), + SearchPanelJson = DefaultSearchPanelJson(), + GroupPanelJson = DefaultGroupPanelJson(), + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.NotificationTemplate)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 620, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List + { + new() { Order = 1, ColCount = 2, ColSpan = 2, ItemType = "group" }, + }), + }); + + #region Notification Template Fields + await _listFormFieldRepository.InsertManyAsync([ + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Code", + CaptionName = "App.Listform.ListformField.Code", + EditGroupOrderNo = 1, + EditOrderNo = 1, + EditorType2 = EditorTypes.dxTextBox, + EditorOptions = EditorOptions.ShowClearButton().MaxLength(64), + ColSpan = 1, + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Name", + CaptionName = "App.Listform.ListformField.Name", + EditGroupOrderNo = 1, + EditOrderNo = 2, + EditorType2 = EditorTypes.dxTextBox, + EditorOptions = EditorOptions.ShowClearButton().MaxLength(128), + ColSpan = 1, + Width = 0, + ListOrderNo = 3, + Visible = true, + IsActive = true, + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Channel", + CaptionName = "App.Listform.ListformField.Channel", + EditGroupOrderNo = 1, + EditOrderNo = 3, + EditorType2 = EditorTypes.dxSelectBox, + EditorOptions = EditorOptions.ShowClearButton(), + ColSpan = 1, + Width = 0, + ListOrderNo = 4, + Visible = true, + IsActive = true, + AllowSearch = true, + // Kanal bos birakilabilir: sablon o zaman butun kanallarda secilebilir. + LookupJson = NotificationChannelLookupJson(), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Boolean, + FieldName = "IsActive", + CaptionName = "App.Listform.ListformField.IsActive", + EditGroupOrderNo = 1, + EditOrderNo = 4, + EditorType2 = EditorTypes.dxCheckBox, + ColSpan = 1, + Width = 0, + ListOrderNo = 5, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Subject", + CaptionName = "App.Listform.ListformField.Subject", + EditGroupOrderNo = 1, + EditOrderNo = 5, + EditorType2 = EditorTypes.dxTextBox, + EditorOptions = EditorOptions.ShowClearButton().MaxLength(256), + ColSpan = 2, + Width = 0, + ListOrderNo = 6, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Body", + CaptionName = "App.Listform.ListformField.Body", + EditGroupOrderNo = 1, + EditOrderNo = 6, + EditorType2 = EditorTypes.dxTextArea, + EditorOptions = EditorOptions.Multiline(220).MaxLength(2048), + ColSpan = 2, + Width = 0, + ListOrderNo = 7, + Visible = true, + IsActive = true, + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + ]); + #endregion + } + #endregion + + #region Manual Notification + listFormName = AppCodes.ManualNotification; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = $"{listFormName}.Description", + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationManual)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson(), + HeaderFilterJson = DefaultHeaderFilterJson(), + SearchPanelJson = DefaultSearchPanelJson(), + GroupPanelJson = DefaultGroupPanelJson(), + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.NotificationManual)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + // Kayit olusur olusmaz gonderim yapilmasi gerektigi icin yazma isi tipli uca + // devredilir: uc hem kaydi yazar hem NotificationManager ile bildirimleri + // uretir. Guncelleme kapali, cunku kayit gonderimin gunlugudur. + InsertServiceAddress = "list-form-dynamic-api/notification-manual-insert", + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 760, 680, true, true, false, true, false), + EditingFormJson = JsonSerializer.Serialize(new List + { + new() { Order = 1, ColCount = 2, ColSpan = 2, ItemType = "group" }, + }), + }); + + #region Manual Notification Fields + await _listFormFieldRepository.InsertManyAsync([ + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.DateTime, + FieldName = "CreationTime", + CaptionName = "App.Listform.ListformField.CreationTime", + EditorType2 = EditorTypes.dxDateBox, + EditorOptions = EditorOptions.DateTime(), + Width = 0, + ListOrderNo = 2, + SortIndex = 0, + SortDirection = GridColumnOptions.SortOrderDesc, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "NotificationTemplateId", + CaptionName = "App.Listform.ListformField.NotificationTemplateId", + EditGroupOrderNo = 1, + EditOrderNo = 1, + EditorType2 = EditorTypes.dxSelectBox, + EditorOptions = EditorOptions.ShowClearButton().Flag("searchEnabled", true), + ColSpan = 2, + Width = 0, + ListOrderNo = 3, + Visible = true, + IsActive = true, + AllowSearch = true, + // Sablon zorunlu degil: konu ve govde elle de yazilabilir. Sablon + // secildiginde konu ve govde secili lookup kaydindan forma tasinir; script + // sablon alanina baglidir, cunku bir alanin scripti yalnizca o alan + // degistiginde calisir. + EditorScript = EditorScript.Build( + EditorScript.Copy("Subject", "Subject").OnChange(), + EditorScript.Copy("Body", "Message").OnChange()), + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.NotificationTemplateValues, + }), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Channels", + CaptionName = "App.Listform.ListformField.Channels", + EditGroupOrderNo = 1, + EditOrderNo = 2, + EditorType2 = EditorTypes.dxTagBox, + EditorOptions = EditorOptions.New() + .Flag("showSelectionControls", true) + .Flag("searchEnabled", true) + .Flag("multiline", true) + .Text("applyValueMode", "useButtons"), + ColSpan = 2, + Width = 0, + ListOrderNo = 4, + Visible = true, + IsActive = true, + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + LookupJson = NotificationChannelLookupJson(), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "UserIds", + CaptionName = "App.Listform.ListformField.UserIds", + EditGroupOrderNo = 1, + EditOrderNo = 3, + EditorType2 = EditorTypes.dxTagBox, + EditorOptions = EditorOptions.New() + .Flag("showSelectionControls", true) + .Flag("searchEnabled", true) + .Flag("multiline", true) + .Text("applyValueMode", "useButtons") + .Number("maxDisplayedTags", 6), + ColSpan = 2, + Width = 0, + ListOrderNo = 5, + Visible = true, + IsActive = true, + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.TenantUserValues, + }), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Subject", + CaptionName = "App.Listform.ListformField.Subject", + EditGroupOrderNo = 1, + EditOrderNo = 4, + EditorType2 = EditorTypes.dxTextBox, + EditorOptions = EditorOptions.ShowClearButton().MaxLength(256), + ColSpan = 2, + Width = 0, + ListOrderNo = 6, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Message", + CaptionName = "App.Listform.ListformField.Message", + EditGroupOrderNo = 1, + EditOrderNo = 5, + EditorType2 = EditorTypes.dxTextArea, + EditorOptions = EditorOptions.Multiline(220).MaxLength(2048), + ColSpan = 2, + Width = 0, + ListOrderNo = 7, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.DateTime, + FieldName = "SentTime", + CaptionName = "App.Listform.ListformField.SentTime", + EditorType2 = EditorTypes.dxDateBox, + EditorOptions = EditorOptions.DateTime(), + Width = 0, + ListOrderNo = 8, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Int32, + FieldName = "SentCount", + CaptionName = "App.Listform.ListformField.SentCount", + EditorType2 = EditorTypes.dxNumberBox, + Width = 0, + ListOrderNo = 9, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Int32, + FieldName = "SkippedCount", + CaptionName = "App.Listform.ListformField.SkippedCount", + EditorType2 = EditorTypes.dxNumberBox, + Width = 0, + ListOrderNo = 10, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "ResultNote", + CaptionName = "App.Listform.ListformField.ResultNote", + EditorType2 = EditorTypes.dxTextBox, + Width = 0, + ListOrderNo = 11, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + ]); + #endregion + } + #endregion + #region Agenda listFormName = AppCodes.Agenda; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs index e75caf42..0da0b4bf 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_DefaultJsons.cs @@ -49,6 +49,29 @@ public static class ListFormSeeder_DefaultJsons }); public static readonly string DefaultFilterJson = "\"IsDeleted\" = 'false'"; + + /// + /// Bildirim kanallarinin sabit listesi. Bildirim ekranlarinin hepsi ayni kumeyi gosterir; + /// kanal eklendiginde tek yerde eklenir (NotificationChannels ile ayni sira). + /// + public static string NotificationChannelLookupJson() => JsonSerializer.Serialize(new LookupDto + { + DataSourceType = UiLookupDataSourceTypeEnum.StaticData, + DisplayExpr = "name", + ValueExpr = "key", + LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] + { + new() { Key = "Sms", Name = "App.Sender.Sms" }, + new() { Key = "Mail", Name = "App.StaticLookup.Mail" }, + new() { Key = "Rocket", Name = "App.StaticLookup.Rocket" }, + new() { Key = "Desktop", Name = "App.StaticLookup.Desktop" }, + new() { Key = "UiActivity", Name = "App.StaticLookup.UiActivity" }, + new() { Key = "UiToast", Name = "App.StaticLookup.UiToast" }, + new() { Key = "WhatsApp", Name = "App.Sender.WhatsApp" }, + new() { Key = "Telegram", Name = "App.Sender.Telegram" }, + }), + }); + public static string EncodeHtmlJson(bool value) => JsonSerializer.Serialize(new { encodeHtml = value }); public static string DefaultFilterRowJson(bool visible = true) => JsonSerializer.Serialize(new GridFilterRowDto { Visible = visible }); public static string DefaultHeaderFilterJson(bool visible = true) => JsonSerializer.Serialize(new { Visible = visible }); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index eaae32ce..75c77067 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -5469,22 +5469,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency AllowSearch = true, ValidationRuleJson = DefaultValidationRuleRequiredJson, - LookupJson = JsonSerializer.Serialize(new LookupDto - { - DataSourceType = UiLookupDataSourceTypeEnum.StaticData, - DisplayExpr = "name", - ValueExpr = "key", - LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { - new () { Key="Sms", Name="App.Sender.Sms" }, - new () { Key="Mail", Name="App.StaticLookup.Mail" }, - new () { Key="WhatsApp", Name="App.Sender.WhatsApp" }, - new () { Key="Telegram", Name="App.Sender.Telegram" }, - new () { Key="Rocket", Name="App.StaticLookup.Rocket" }, - new () { Key="UiActivity", Name="App.StaticLookup.UiActivity" }, - new () { Key="UiToast", Name="App.StaticLookup.UiToast" }, - new () { Key="Desktop", Name="App.StaticLookup.Desktop" }, - }), - }), + LookupJson = NotificationChannelLookupJson(), ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson @@ -5687,22 +5672,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency IsActive = true, AllowSearch = true, - LookupJson = JsonSerializer.Serialize(new LookupDto - { - DataSourceType = UiLookupDataSourceTypeEnum.StaticData, - DisplayExpr = "name", - ValueExpr = "key", - LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { - new () { Key="Sms", Name="App.Sender.Sms" }, - new () { Key="Mail", Name="App.StaticLookup.Mail" }, - new () { Key="Rocket", Name="App.StaticLookup.Rocket" }, - new () { Key="Desktop", Name="App.StaticLookup.Desktop" }, - new () { Key="UiActivity", Name="App.StaticLookup.UiActivity" }, - new () { Key="UiToast", Name="App.StaticLookup.UiToast" }, - new () { Key="WhatsApp", Name="App.Sender.WhatsApp" }, - new () { Key="Telegram", Name="App.Sender.Telegram" }, - }), - }), + LookupJson = NotificationChannelLookupJson(), ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson @@ -5757,6 +5727,23 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson }, + new() + { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Subject", + CaptionName = "App.Listform.ListformField.Subject", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, new() { ListFormCode = listForm.ListFormCode, CultureName = LanguageCodes.En, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index c0c4eb27..c8e3bd29 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -9,6 +9,8 @@ public enum TableNameEnum Notification, NotificationRule, NotificationType, + NotificationTemplate, + NotificationManual, BackgroundWorker, BackgroundWorker_MailQueue, BackgroundWorker_MailQueueEvents, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index d9f35592..59561607 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -456,6 +456,19 @@ public static class PlatformConsts public const string NotificationTypes = Default + ".NotificationTypes"; public const string NotificationRules = Default + ".NotificationRules"; public const string Notification = Default + ".Notification"; + public const string NotificationTemplates = Default + ".NotificationTemplates"; + } + + /// + /// Elle bildirim gonderim ekrani (Administration menusu). + /// ekrani gorme yetkisidir; gonderimin kendisi + /// ister, cunku kayit olusturmak gonderim demektir. + /// + public const string ManualNotification = Prefix.App + ".ManualNotification"; + + public static class ManualNotifications + { + public const string Create = ManualNotification + ".Create"; } public static class BackgroundWorkers diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index 90a0deaf..85329be2 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -86,6 +86,8 @@ public static class TableNameResolver { nameof(TableNameEnum.VideoroomChat), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.Sequence), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.Agenda), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.NotificationTemplate), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.NotificationManual), (TablePrefix.TenantByName, MenuPrefix.Administration) }, // 🔹 INTRANET TABLOLARI { nameof(TableNameEnum.IntranetWidget), (TablePrefix.TenantByName, MenuPrefix.Sas) }, diff --git a/api/src/Sozsoft.Platform.Domain/BackgroundWorkers/NotificationWorker.cs b/api/src/Sozsoft.Platform.Domain/BackgroundWorkers/NotificationWorker.cs index 7cd490a5..a630ab7a 100644 --- a/api/src/Sozsoft.Platform.Domain/BackgroundWorkers/NotificationWorker.cs +++ b/api/src/Sozsoft.Platform.Domain/BackgroundWorkers/NotificationWorker.cs @@ -116,7 +116,8 @@ public class NotificationWorker : BackgroundWorkerBase return; } - // Konu satirinda bildirim tipinin adi gecer; navigasyon yuklenmezse konu bos kalirdi. + // Konu kaydin kendisinden gelir; yoksa bildirim tipinin adina duselir, bu yuzden + // navigasyon yuklenir (includeDetails) - yuklenmezse konu bos kalirdi. await SendChannelAsync( NotificationChannels.Mail, async notification => (await ErpEmailSender.SendEmailAsync( @@ -124,7 +125,9 @@ public class NotificationWorker : BackgroundWorkerBase new KeyValuePair(senderName ?? senderEmail!, senderEmail!), new { }, notification.Message, - $"Platform Bildirim: {notification.NotificationType?.Name}")).Success, + notification.Subject.IsNullOrWhiteSpace() + ? $"Platform Bildirim: {notification.NotificationType?.Name}" + : notification.Subject)).Success, cancellationToken, includeDetails: true); } diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index 135437ac..1180b05f 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -410,6 +410,7 @@ public static class SeedConsts public const string NotificationTypes = Default + ".NotificationTypes"; public const string NotificationRules = Default + ".NotificationRules"; public const string Notification = Default + ".Notification"; + public const string NotificationTemplates = Default + ".NotificationTemplates"; } public static class BackgroundWorkers @@ -498,6 +499,7 @@ public static class SeedConsts public const string DevRequest = Prefix.App + ".DevRequest"; public const string DevRequests = Prefix.App + ".DevRequests"; public const string Agenda = Prefix.App + ".Agenda"; + public const string ManualNotification = Prefix.App + ".ManualNotification"; } public static class DataSources diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationManual.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationManual.cs new file mode 100644 index 00000000..28cce69c --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationManual.cs @@ -0,0 +1,36 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +/// +/// Elle yapilan bildirim gonderiminin kaydi. Kayit olusur olusmaz ekranin +/// InsertAfterCommand kancasi Adm_T_NotificationManual_Dispatch prosedurunu cagirir; +/// prosedur alici x kanal acilimini Sas_H_Notification kuyruguna "Manuel" bildirim tipiyle +/// yazar ve sonuc sayaclarini bu kayda geri isler. Kayit gonderimin kendisi degil, gunlugudur. +/// +public class NotificationManual : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; set; } + + public Guid? NotificationTemplateId { get; set; } + + /// Secilen kanallar; cok degerli alan sozlesmesine gore | ile saklanir. + public string Channels { get; set; } + + /// Secilen kullanici kimlikleri; | ile saklanir. + public string UserIds { get; set; } + + public string Subject { get; set; } + public string Message { get; set; } + + public DateTime? SentTime { get; set; } + public int SentCount { get; set; } + + /// Ilgili kanalda adresi (e-posta, telefon, chat id) olmadigi icin atlanan alici sayisi. + public int SkippedCount { get; set; } + + /// Atlananlarin kanal bazli gerekcesi; bos ise herkese gonderildi. + public string ResultNote { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationTemplate.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationTemplate.cs new file mode 100644 index 00000000..99153e33 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/NotificationTemplate.cs @@ -0,0 +1,30 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +/// +/// Manuel bildirim gonderiminde secilebilen mesaj sablonu. Govde, gonderim aninda alici bazli +/// yer tutucularla ({AdSoyad}, {KullaniciAdi}, {Eposta}, {Tarih}, +/// {Saat}, {Kiraci}) cozulur. +/// +public class NotificationTemplate : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; set; } + + public string Code { get; set; } + public string Name { get; set; } + + /// Sablonun hangi kanal icin oldugu; bos ise tum kanallarda kullanilabilir. + public string Channel { get; set; } + + /// + /// Mail konusu. Gonderimde bildirim kaydinin Subject kolonuna yazilir; bos ise + /// NotificationWorker konuyu bildirim tipinin adindan uretir. + /// + public string Subject { get; set; } + + public string Body { get; set; } + public bool IsActive { get; set; } +} diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index c7dd7079..5f76f9a8 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -102,6 +102,8 @@ public class PlatformDbContext : public DbSet SkillTypes { get; set; } public DbSet DevRequests { get; set; } public DbSet Agendas { get; set; } + public DbSet NotificationManuals { get; set; } + public DbSet NotificationTemplates { get; set; } // Dynamic Services public DbSet Skills { get; set; } @@ -1593,6 +1595,42 @@ public class PlatformDbContext : b.Property(e => e.RecurrenceException).HasColumnType("nvarchar(max)"); }); + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationTemplate)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(e => e.Code).HasMaxLength(64).IsRequired(); + b.Property(e => e.Name).HasMaxLength(128).IsRequired(); + b.Property(e => e.Channel).HasMaxLength(64); + b.Property(e => e.Subject).HasMaxLength(256); + + // Govde, bildirim kaydinin Message kolonuyla ayni sinirdadir; daha uzun bir sablon + // gonderimde kirpilirdi. + b.Property(e => e.Body).HasMaxLength(2048).IsRequired(); + b.Property(e => e.IsActive).HasDefaultValue(true); + + b.HasIndex(e => new { e.TenantId, e.Code }) + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + }); + + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationManual)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(e => e.Channels).HasMaxLength(256).IsRequired(); + b.Property(e => e.UserIds).HasColumnType("nvarchar(max)").IsRequired(); + b.Property(e => e.Subject).HasMaxLength(256); + b.Property(e => e.Message).HasMaxLength(2048); + b.Property(e => e.SentCount).HasDefaultValue(0); + b.Property(e => e.SkippedCount).HasDefaultValue(0); + b.Property(e => e.ResultNote).HasMaxLength(1024); + + b.HasIndex(e => new { e.TenantId, e.CreationTime }); + }); + ConfigureProviderSpecificModel(builder); } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.Designer.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.Designer.cs index a928906d..a54ebc26 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260920201729_Initial")] + [Migration("20260921060400_Initial")] partial class Initial { /// @@ -577,6 +577,10 @@ namespace Sozsoft.Platform.Migrations b.Property("ReadTime") .HasColumnType("datetime2"); + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("UserId") .HasColumnType("uniqueidentifier"); @@ -4303,6 +4307,165 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_Note", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.NotificationManual", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Channels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Message") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("NotificationTemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResultNote") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SentCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("SkippedCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CreationTime"); + + b.ToTable("Adm_T_NotificationManual", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Channel") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Adm_T_NotificationTemplate", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => { b.Property("Id") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.cs index 414557b1..89899aa2 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260920201729_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260921060400_Initial.cs @@ -677,6 +677,59 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Adm_T_Note", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Adm_T_NotificationManual", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + NotificationTemplateId = table.Column(type: "uniqueidentifier", nullable: true), + Channels = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + UserIds = table.Column(type: "nvarchar(max)", nullable: false), + Subject = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Message = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), + SentTime = table.Column(type: "datetime2", nullable: true), + SentCount = table.Column(type: "int", nullable: false, defaultValue: 0), + SkippedCount = table.Column(type: "int", nullable: false, defaultValue: 0), + ResultNote = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_NotificationManual", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Adm_T_NotificationTemplate", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + Code = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + Channel = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + Subject = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Body = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_NotificationTemplate", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Adm_T_ReportCategory", columns: table => new @@ -3412,6 +3465,7 @@ namespace Sozsoft.Platform.Migrations Identifier = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), UserId = table.Column(type: "uniqueidentifier", nullable: true), Message = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + Subject = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), IsSent = table.Column(type: "bit", nullable: false), IsRead = table.Column(type: "bit", nullable: false), ReadTime = table.Column(type: "datetime2", nullable: true), @@ -3827,6 +3881,18 @@ namespace Sozsoft.Platform.Migrations table: "Adm_T_MessengerConversationMessage", columns: new[] { "TenantId", "ConversationId", "SentAt" }); + migrationBuilder.CreateIndex( + name: "IX_Adm_T_NotificationManual_TenantId_CreationTime", + table: "Adm_T_NotificationManual", + columns: new[] { "TenantId", "CreationTime" }); + + migrationBuilder.CreateIndex( + name: "IX_Adm_T_NotificationTemplate_TenantId_Code", + table: "Adm_T_NotificationTemplate", + columns: new[] { "TenantId", "Code" }, + unique: true, + filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( name: "IX_Adm_T_ReportCategory_TenantId_Name", table: "Adm_T_ReportCategory", @@ -4444,6 +4510,12 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Adm_T_Note"); + migrationBuilder.DropTable( + name: "Adm_T_NotificationManual"); + + migrationBuilder.DropTable( + name: "Adm_T_NotificationTemplate"); + migrationBuilder.DropTable( name: "Adm_T_ReportTemplate"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index f76353bf..e73fed84 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -574,6 +574,10 @@ namespace Sozsoft.Platform.Migrations b.Property("ReadTime") .HasColumnType("datetime2"); + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("UserId") .HasColumnType("uniqueidentifier"); @@ -4300,6 +4304,165 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_Note", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.NotificationManual", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Channels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Message") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("NotificationTemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResultNote") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SentCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("SkippedCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CreationTime"); + + b.ToTable("Adm_T_NotificationManual", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.NotificationTemplate", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Channel") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Adm_T_NotificationTemplate", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => { b.Property("Id") diff --git a/configs/seeds/host/data/Abp.Identity.Permissions.json b/configs/seeds/host/data/Abp.Identity.Permissions.json index 0ff99ec0..722626f8 100644 --- a/configs/seeds/host/data/Abp.Identity.Permissions.json +++ b/configs/seeds/host/data/Abp.Identity.Permissions.json @@ -3604,12 +3604,132 @@ "MultiTenancySide": 3, "Modules": "Sas|Adm" }, + { + "GroupName": "App.Administration", + "Name": "App.Administration.Notifications", + "ParentName": null, + "DisplayName": "App.Administration.Notifications", + "Order": 5, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.ManualNotification", + "ParentName": "App.Administration.Notifications", + "DisplayName": "App.ManualNotification", + "Order": 1, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.ManualNotification.Create", + "ParentName": "App.ManualNotification", + "DisplayName": "App.Platform.Create", + "Order": 1, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.ManualNotification.Update", + "ParentName": "App.ManualNotification", + "DisplayName": "App.Platform.Update", + "Order": 2, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.ManualNotification.Delete", + "ParentName": "App.ManualNotification", + "DisplayName": "App.Platform.Delete", + "Order": 3, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.ManualNotification.Export", + "ParentName": "App.ManualNotification", + "DisplayName": "Export", + "Order": 4, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates", + "ParentName": "App.Administration.Notifications", + "DisplayName": "App.Notifications.NotificationTemplates", + "Order": 2, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates.Create", + "ParentName": "App.Notifications.NotificationTemplates", + "DisplayName": "App.Platform.Create", + "Order": 1, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates.Delete", + "ParentName": "App.Notifications.NotificationTemplates", + "DisplayName": "App.Platform.Delete", + "Order": 2, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates.Export", + "ParentName": "App.Notifications.NotificationTemplates", + "DisplayName": "Export", + "Order": 3, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates.Import", + "ParentName": "App.Notifications.NotificationTemplates", + "DisplayName": "Import", + "Order": 4, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Administration", + "Name": "App.Notifications.NotificationTemplates.Update", + "ParentName": "App.Notifications.NotificationTemplates", + "DisplayName": "App.Platform.Update", + "Order": 5, + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, { "GroupName": "App.Administration", "Name": "App.Agenda", "ParentName": null, "DisplayName": "App.Agenda", - "Order": 5, + "Order": 6, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" @@ -3669,7 +3789,7 @@ "Name": "App.Setting", "ParentName": null, "DisplayName": "App.Setting", - "Order": 6, + "Order": 7, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" @@ -3919,7 +4039,7 @@ "Name": "App.Intranet", "ParentName": null, "DisplayName": "App.Intranet", - "Order": 7, + "Order": 8, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" @@ -5149,7 +5269,7 @@ "Name": "App.Platform.Restrictions", "ParentName": null, "DisplayName": "App.Platform.Restrictions", - "Order": 8, + "Order": 9, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" @@ -5279,7 +5399,7 @@ "Name": "Abp.Identity", "ParentName": null, "DisplayName": "Abp.Identity", - "Order": 9, + "Order": 10, "IsEnabled": true, "MultiTenancySide": 2, "Modules": "Sas|Adm" @@ -5879,7 +5999,7 @@ "Name": "App.Videoroom", "ParentName": null, "DisplayName": "App.Videoroom", - "Order": 10, + "Order": 11, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" @@ -5909,7 +6029,7 @@ "Name": "App.ForumManagement.Publish", "ParentName": null, "DisplayName": "App.ForumManagement.Publish", - "Order": 11, + "Order": 12, "IsEnabled": true, "MultiTenancySide": 3, "Modules": "Sas|Adm" diff --git a/configs/seeds/host/data/App.Menus.Menu.json b/configs/seeds/host/data/App.Menus.Menu.json index 2432bea2..329c08f7 100644 --- a/configs/seeds/host/data/App.Menus.Menu.json +++ b/configs/seeds/host/data/App.Menus.Menu.json @@ -775,11 +775,44 @@ "IsDisabled": false, "ModuleId": "Adm" }, + { + "ParentCode": "App.Administration", + "Code": "App.Administration.Notifications", + "DisplayName": "App.Administration.Notifications", + "Order": 4, + "Url": null, + "Icon": "FcWorkflow", + "RequiredPermissionName": null, + "IsDisabled": false, + "ModuleId": "Adm" + }, + { + "ParentCode": "App.Administration.Notifications", + "Code": "App.Notifications.NotificationTemplates", + "DisplayName": "App.Notifications.NotificationTemplates", + "Order": 1, + "Url": "/admin/list/App.Notifications.NotificationTemplates", + "Icon": "FcDocument", + "RequiredPermissionName": "App.Notifications.NotificationTemplates", + "IsDisabled": false, + "ModuleId": "Adm" + }, + { + "ParentCode": "App.Administration.Notifications", + "Code": "App.ManualNotification", + "DisplayName": "App.ManualNotification", + "Order": 2, + "Url": "/admin/list/App.ManualNotification", + "Icon": "FcSms", + "RequiredPermissionName": "App.ManualNotification", + "IsDisabled": false, + "ModuleId": "Adm" + }, { "ParentCode": "App.Administration", "Code": "App.Agenda", "DisplayName": "App.Agenda", - "Order": 4, + "Order": 5, "Url": "/admin/list/App.Agenda", "Icon": "FcPlanner", "RequiredPermissionName": "App.Agenda", @@ -790,7 +823,7 @@ "ParentCode": "App.Administration", "Code": "App.Setting", "DisplayName": "App.Setting", - "Order": 5, + "Order": 6, "Url": "/admin/settings", "Icon": "FcSettings", "RequiredPermissionName": "App.Setting", @@ -801,7 +834,7 @@ "ParentCode": "App.Administration", "Code": "App.Administration.Intranet", "DisplayName": "App.Intranet", - "Order": 6, + "Order": 7, "Url": null, "Icon": "FcConferenceCall", "RequiredPermissionName": null, @@ -900,7 +933,7 @@ "ParentCode": "App.Administration", "Code": "App.Administration.Restrictions", "DisplayName": "App.Platform.Restrictions", - "Order": 7, + "Order": 8, "Url": null, "Icon": "FaLock", "RequiredPermissionName": null, @@ -933,7 +966,7 @@ "ParentCode": "App.Administration", "Code": "Abp.Identity", "DisplayName": "Abp.Identity", - "Order": 8, + "Order": 9, "Url": null, "Icon": "FcConferenceCall", "RequiredPermissionName": null, @@ -1032,7 +1065,7 @@ "ParentCode": "App.Administration", "Code": "App.Reports.Management", "DisplayName": "App.Reports.Management", - "Order": 9, + "Order": 10, "Url": null, "Icon": "FcDocument", "RequiredPermissionName": null, @@ -1065,7 +1098,7 @@ "ParentCode": "App.Administration", "Code": "App.Videoroom", "DisplayName": "App.Videoroom", - "Order": 10, + "Order": 11, "Url": "/admin/videoroom/dashboard", "Icon": "FcVideoCall", "RequiredPermissionName": "App.Videoroom", @@ -1076,7 +1109,7 @@ "ParentCode": "App.Administration", "Code": "App.Forum", "DisplayName": "App.Forum", - "Order": 11, + "Order": 12, "Url": "/admin/forum", "Icon": "FcLink", "RequiredPermissionName": "App.ForumManagement.Publish", diff --git a/configs/seeds/host/data/App.Notifications.NotificationRules.json b/configs/seeds/host/data/App.Notifications.NotificationRules.json index 099e94b2..1335ac81 100644 --- a/configs/seeds/host/data/App.Notifications.NotificationRules.json +++ b/configs/seeds/host/data/App.Notifications.NotificationRules.json @@ -33,6 +33,16 @@ "IsActive": true, "IsFixed": false, "IsCustomized": false + }, + { + "Id": "7d2e5a90-1c84-5b37-9e62-8a4f0d3b6c22", + "NotificationTypeId": "3f6c8b41-9d27-5a63-8f1e-4b0a9c6d2e11", + "RecipientType": "Custom", + "RecipientId": null, + "Channel": "UiActivity", + "IsActive": false, + "IsFixed": false, + "IsCustomized": false } ] } diff --git a/configs/seeds/host/data/App.Notifications.NotificationTemplates.json b/configs/seeds/host/data/App.Notifications.NotificationTemplates.json new file mode 100644 index 00000000..9674a7b6 --- /dev/null +++ b/configs/seeds/host/data/App.Notifications.NotificationTemplates.json @@ -0,0 +1,44 @@ +{ + "ListFormCode": "App.Notifications.NotificationTemplates", + "KeyFieldName": "Id", + "GeneratedAt": "2026-09-20T21:00:00Z", + "Order": 3, + "Rows": [ + { + "Id": "b1f9a0d2-6c41-5f8e-9a37-2d5c7e1b4a01", + "Code": "GENEL-DUYURU", + "Name": "Genel Duyuru", + "Channel": null, + "Subject": "Duyuru", + "Body": "Sayın {AdSoyad},\n\n{Kiraci} yönetimi tarafından bir duyuru paylaşıldı. Ayrıntılar için uygulamadaki duyurular ekranını inceleyin.\n\n{Tarih} {Saat}", + "IsActive": true + }, + { + "Id": "b1f9a0d2-6c41-5f8e-9a37-2d5c7e1b4a02", + "Code": "BAKIM-BILDIRIMI", + "Name": "Planlı Bakım Bildirimi", + "Channel": "Mail", + "Subject": "Planlı bakım çalışması", + "Body": "Sayın {AdSoyad},\n\n{Tarih} {Saat} itibarıyla planlı bakım çalışması başlayacaktır. Çalışma süresince uygulamaya erişim kesintili olabilir.\n\nİyi çalışmalar.", + "IsActive": true + }, + { + "Id": "b1f9a0d2-6c41-5f8e-9a37-2d5c7e1b4a03", + "Code": "SMS-KISA-BILGI", + "Name": "Kısa Bilgi (SMS)", + "Channel": "Sms", + "Subject": null, + "Body": "{AdSoyad}: {Kiraci} uygulamasinda sizi bekleyen bir islem var. {Tarih} {Saat}", + "IsActive": true + }, + { + "Id": "b1f9a0d2-6c41-5f8e-9a37-2d5c7e1b4a04", + "Code": "TOPLANTI-HATIRLATMA", + "Name": "Toplantı Hatırlatma", + "Channel": null, + "Subject": "Toplantı hatırlatması", + "Body": "Sayın {AdSoyad},\n\n{Tarih} günü {Saat} saatindeki toplantıyı hatırlatırız. Katılamayacaksanız organizatöre bilgi verin.", + "IsActive": true + } + ] +} diff --git a/configs/seeds/host/data/App.Notifications.NotificationTypes.json b/configs/seeds/host/data/App.Notifications.NotificationTypes.json index 4689c0f2..96779379 100644 --- a/configs/seeds/host/data/App.Notifications.NotificationTypes.json +++ b/configs/seeds/host/data/App.Notifications.NotificationTypes.json @@ -18,6 +18,11 @@ "Id": "78a4b82b-de80-5826-bf9b-e49a2436cd43", "Name": "YeniKullanici", "Description": "Sisteme yeni bir kullanıcı eklendiğinde tetiklenir. Yönetici ekibin yetki ve rol atamasını yapması için haberdar olmasını sağlar." + }, + { + "Id": "3f6c8b41-9d27-5a63-8f1e-4b0a9c6d2e11", + "Name": "Manuel", + "Description": "Manuel Bildirim ekranindan elle yapilan gonderimlerin tipi. Kural motorundan gecmez: alicilar ve kanallar ekranda secilir, her gonderim bu tiple kuyruga yazilir." } ] }