Manuel Notification eklendi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-21 09:57:02 +03:00
parent 969b048946
commit 722decd040
30 changed files with 1772 additions and 55 deletions

View file

@ -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. |

View file

@ -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ı |

View file

@ -3,6 +3,12 @@
public static class NotificationConsts
{
public const string AppName = "Notifications";
/// <summary>
/// Elle yapilan gonderimlerin bildirim tipi. Kural motorundan gecmeyen her bildirim bu tiple
/// yazilir; tip kaydi seed ile gelir (<c>App.Notifications.NotificationTypes.json</c>).
/// </summary>
public const string ManualTypeName = "Manuel";
}
public static class NotificationRecipientTypes
@ -14,3 +20,16 @@ public static class NotificationRecipientTypes
public const string Custom = "Custom";
}
/// <summary>
/// Manuel gonderimde mesaj icinde cozulen yer tutucular. Sablon metni bu anahtarlari tasir,
/// gonderim aninda her alici icin ayri ayri degistirilir.
/// </summary>
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}";
}

View file

@ -15,6 +15,13 @@ public class Notification : FullAuditedEntity<Guid>
public Guid? UserId { get; set; }
public required string Message { get; set; }
/// <summary>
/// Mail kanalinin konu satiri. Bos ise <c>NotificationWorker</c> konuyu bildirim tipinin
/// adindan uretir; kural motorundan gecen bildirimlerde bos kalir, manuel gonderimde
/// ekrandan ya da sablondan gelir.
/// </summary>
public string? Subject { get; set; }
public bool IsSent { get; set; }
public bool IsRead { get; set; }
public DateTime? ReadTime { get; set; }

View file

@ -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 <c>NotificationWorker</c>'a aittir.
/// </summary>
Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage);
/// <summary>
/// Alici kumesi kurallardan degil ekrandan gelen gonderim: secilen her kullanici icin secilen
/// her kanalda bir bildirim kaydi uretir. Kanaldan cikis yine <c>NotificationWorker</c>'a
/// aittir; kullanici tercihi dikkate alinmaz, cunku gonderim zaten bilincli bir aksiyondur.
/// </summary>
Task<ManualNotificationResult> SendManualNotificationAsync(ManualNotificationRequest request);
}
/// <summary>Manuel gonderim istegi; alicilar ve kanallar cagirandan gelir.</summary>
public class ManualNotificationRequest
{
public Guid NotificationTypeId { get; set; }
public IReadOnlyCollection<Guid> UserIds { get; set; } = [];
public IReadOnlyCollection<string> Channels { get; set; } = [];
public string Message { get; set; } = string.Empty;
/// <summary>Mail konu satiri; bos ise konu bildirim tipinin adindan uretilir.</summary>
public string Subject { get; set; }
}
/// <summary>
/// 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.
/// </summary>
public class ManualNotificationResult
{
public int SentCount { get; set; }
public Dictionary<string, int> 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<NotificationRule> repository,
@ -37,7 +72,9 @@ public class NotificationManager : INotificationManager
IRepository<Notification, Guid> 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);
}
/// <summary>
/// Alicilari kurallar yerine cagiranin verdigi gonderim. Kullanici tercihi okunmaz: manuel
/// gonderim zorunlu bildirim (<c>IsFixed</c>) gibi davranir, aksi halde yonetici bilincli
/// yaptigi gonderimin kime gitmedigini hic ogrenemezdi.
/// </summary>
public async Task<ManualNotificationResult> 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<Notification>();
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;
}
/// <summary>Mesajdaki yer tutucular alici bazinda cozulur; her alici kendi metnini alir.</summary>
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;
}
/// <summary>
/// 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.
/// </summary>
private async Task SaveAndAnnounceAsync(List<Notification> 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))]);
}
/// <summary>Yer tutucularin cozulmesi icin gereken alici bilgisi.</summary>
private sealed record ManualRecipient(Guid Id, string UserName, string Name, string Surname, string Email);
/// <summary>Kuralin hedefledigi kullanicilar; <c>Custom</c> alici bu yoldan gecmez.</summary>
private async Task<List<Guid>> ResolveRecipientUserIdsAsync(NotificationRule notificationRule, IQueryable<IdentityUser> queryUsers)
{

View file

@ -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)

View file

@ -0,0 +1,23 @@
using System;
using System.Text.Json.Serialization;
namespace Sozsoft.Platform.ListForms.DynamicApi;
/// <summary>
/// Manuel bildirim gonderim ekraninin kayit girdisi. <see cref="Channels"/> ve
/// <see cref="UserIds"/> ekranda <c>dxTagBox</c> ile secilir; tipli DTO yolunda
/// <c>QueryHelper</c> devreye girmedigi icin dizi/metin donusumu converter ile yapilir.
/// </summary>
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; }
}

View file

@ -13,4 +13,5 @@ public interface IListFormDynamicApiAppService
Task PostTenantUpdateAsync(DynamicApiBaseInput<CreateUpdateTenantInput> input);
Task PostTenantDeleteAsync(DynamicApiBaseInput<string> input);
Task<Guid> PostCreateTenantFromOrderAsync(DynamicApiBaseInput<CreateTenantFromOrderInput> input);
Task<Guid> PostNotificationManualInsertAsync(DynamicApiBaseInput<CreateNotificationManualInput> input);
}

View file

@ -146,6 +146,38 @@ public static class LookupQueryValues
$"FROM \"AbpRoles\"" +
$"ORDER BY \"Name\"";
/// <summary>
/// Manuel bildirim alicilari: yalnizca oturumun kiracisindaki aktif kullanicilar. Kiraci
/// filtresi '@TENANTID' ile kurulur, host baglaminda "IS NULL" olarak cozulur.
/// </summary>
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\";";
/// <summary>
/// Manuel gonderimde secilebilen aktif sablonlar. Konu ve govde de secilir: ekrandaki
/// <c>copy</c> kurali sablon secilince bu kolonlari forma tasir (EditorScript).
/// </summary>
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\", " +

View file

@ -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> identityOptions;
private readonly IRepository<Order, Guid> orderRepository;
private readonly IListFormSeedDataSynchronizer seedDataSynchronizer;
private readonly IRepository<NotificationManual, Guid> notificationManualRepository;
private readonly IRepository<NotificationTemplate, Guid> notificationTemplateRepository;
private readonly IReadOnlyRepository<Sozsoft.Notifications.Entities.NotificationType> notificationTypeRepository;
private readonly INotificationManager notificationManager;
public ListFormDynamicApiAppService(
ITenantRepository tenantRepository,
@ -46,7 +52,11 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
IConfiguration configuration,
IOptions<IdentityOptions> identityOptions,
IRepository<Order, Guid> orderRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer)
IListFormSeedDataSynchronizer seedDataSynchronizer,
IRepository<NotificationManual, Guid> notificationManualRepository,
IRepository<NotificationTemplate, Guid> notificationTemplateRepository,
IReadOnlyRepository<Sozsoft.Notifications.Entities.NotificationType> 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);
}
}
/// <summary>
/// Manuel bildirim gonderimi. Kayit olusur olusmaz gonderim yapilir: kayit gonderimin kendisi
/// degil gunlugudur. Gonderim yolu Bildirim Kurallari ekranindaki elle gonderimle aynidir
/// (<c>INotificationManager</c>); tek fark alici kumesinin kurallardan degil ekrandan gelmesi.
/// </summary>
[Authorize(PlatformConsts.AppCodes.ManualNotifications.Create)]
public async Task<Guid> PostNotificationManualInsertAsync(DynamicApiBaseInput<CreateNotificationManualInput> 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<string> SplitMultiValue(string value)
{
return [.. (value ?? string.Empty)
.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
}
/// <summary>
/// Atlananlarin kanal bazli gerekcesi. Adresi olmayan alici gonderimi durdurmaz; gonderen
/// kimin alamadigini kayitta gorur, cozum kullanici kaydindadir.
/// </summary>
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));
}
}

View file

@ -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ş"
}
]
}

View file

@ -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<EditingFormDto>
{
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<EditingFormDto>
{
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))

View file

@ -49,6 +49,29 @@ public static class ListFormSeeder_DefaultJsons
});
public static readonly string DefaultFilterJson = "\"IsDeleted\" = 'false'";
/// <summary>
/// Bildirim kanallarinin sabit listesi. Bildirim ekranlarinin hepsi ayni kumeyi gosterir;
/// kanal eklendiginde tek yerde eklenir (<c>NotificationChannels</c> ile ayni sira).
/// </summary>
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 });

View file

@ -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,

View file

@ -9,6 +9,8 @@ public enum TableNameEnum
Notification,
NotificationRule,
NotificationType,
NotificationTemplate,
NotificationManual,
BackgroundWorker,
BackgroundWorker_MailQueue,
BackgroundWorker_MailQueueEvents,

View file

@ -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";
}
/// <summary>
/// Elle bildirim gonderim ekrani (Administration menusu). <see cref="ManualNotification"/>
/// ekrani gorme yetkisidir; gonderimin kendisi <see cref="ManualNotifications.Create"/>
/// ister, cunku kayit olusturmak gonderim demektir.
/// </summary>
public const string ManualNotification = Prefix.App + ".ManualNotification";
public static class ManualNotifications
{
public const string Create = ManualNotification + ".Create";
}
public static class BackgroundWorkers

View file

@ -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) },

View file

@ -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<string, string>(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);
}

View file

@ -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

View file

@ -0,0 +1,36 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities;
/// <summary>
/// Elle yapilan bildirim gonderiminin kaydi. Kayit olusur olusmaz ekranin
/// <c>InsertAfterCommand</c> kancasi <c>Adm_T_NotificationManual_Dispatch</c> prosedurunu cagirir;
/// prosedur alici x kanal acilimini <c>Sas_H_Notification</c> kuyruguna "Manuel" bildirim tipiyle
/// yazar ve sonuc sayaclarini bu kayda geri isler. Kayit gonderimin kendisi degil, gunlugudur.
/// </summary>
public class NotificationManual : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid? NotificationTemplateId { get; set; }
/// <summary>Secilen kanallar; cok degerli alan sozlesmesine gore <c>|</c> ile saklanir.</summary>
public string Channels { get; set; }
/// <summary>Secilen kullanici kimlikleri; <c>|</c> ile saklanir.</summary>
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; }
/// <summary>Ilgili kanalda adresi (e-posta, telefon, chat id) olmadigi icin atlanan alici sayisi.</summary>
public int SkippedCount { get; set; }
/// <summary>Atlananlarin kanal bazli gerekcesi; bos ise herkese gonderildi.</summary>
public string ResultNote { get; set; }
}

View file

@ -0,0 +1,30 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities;
/// <summary>
/// Manuel bildirim gonderiminde secilebilen mesaj sablonu. Govde, gonderim aninda alici bazli
/// yer tutucularla (<c>{AdSoyad}</c>, <c>{KullaniciAdi}</c>, <c>{Eposta}</c>, <c>{Tarih}</c>,
/// <c>{Saat}</c>, <c>{Kiraci}</c>) cozulur.
/// </summary>
public class NotificationTemplate : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public string Code { get; set; }
public string Name { get; set; }
/// <summary>Sablonun hangi kanal icin oldugu; bos ise tum kanallarda kullanilabilir.</summary>
public string Channel { get; set; }
/// <summary>
/// Mail konusu. Gonderimde bildirim kaydinin <c>Subject</c> kolonuna yazilir; bos ise
/// <c>NotificationWorker</c> konuyu bildirim tipinin adindan uretir.
/// </summary>
public string Subject { get; set; }
public string Body { get; set; }
public bool IsActive { get; set; }
}

View file

@ -102,6 +102,8 @@ public class PlatformDbContext :
public DbSet<SkillType> SkillTypes { get; set; }
public DbSet<DevRequest> DevRequests { get; set; }
public DbSet<Agenda> Agendas { get; set; }
public DbSet<NotificationManual> NotificationManuals { get; set; }
public DbSet<NotificationTemplate> NotificationTemplates { get; set; }
// Dynamic Services
public DbSet<Skill> Skills { get; set; }
@ -1593,6 +1595,42 @@ public class PlatformDbContext :
b.Property(e => e.RecurrenceException).HasColumnType("nvarchar(max)");
});
builder.Entity<NotificationTemplate>(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<NotificationManual>(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);
}

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260920201729_Initial")]
[Migration("20260921060400_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -577,6 +577,10 @@ namespace Sozsoft.Platform.Migrations
b.Property<DateTime?>("ReadTime")
.HasColumnType("datetime2");
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("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<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Channels")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<Guid?>("NotificationTemplateId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ResultNote")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<int>("SentCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<DateTime?>("SentTime")
.HasColumnType("datetime2");
b.Property<int>("SkippedCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("Channel")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("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<Guid>("Id")

View file

@ -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<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
NotificationTemplateId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Channels = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
UserIds = table.Column<string>(type: "nvarchar(max)", nullable: false),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Message = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
SentTime = table.Column<DateTime>(type: "datetime2", nullable: true),
SentCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
SkippedCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
ResultNote = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Code = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Channel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Body = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Message = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
IsSent = table.Column<bool>(type: "bit", nullable: false),
IsRead = table.Column<bool>(type: "bit", nullable: false),
ReadTime = table.Column<DateTime>(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");

View file

@ -574,6 +574,10 @@ namespace Sozsoft.Platform.Migrations
b.Property<DateTime?>("ReadTime")
.HasColumnType("datetime2");
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("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<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Channels")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Message")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<Guid?>("NotificationTemplateId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ResultNote")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<int>("SentCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<DateTime?>("SentTime")
.HasColumnType("datetime2");
b.Property<int>("SkippedCount")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("Channel")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Subject")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("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<Guid>("Id")

View file

@ -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"

View file

@ -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",

View file

@ -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
}
]
}

View file

@ -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
}
]
}

View file

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