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."
}
]
}