Notification ve BackgroundWorker güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-09-22 12:48:14 +03:00
parent 660f6db268
commit 8beeaeb8d5
38 changed files with 1830 additions and 1845 deletions

View file

@ -1182,8 +1182,7 @@ soneklerini alır.
| **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Ayarlanabilir olması gereken değeri koda gömme. |
| **Languages › Language / Language Text** | `App.Languages.Language` · `.LanguageText` | `Sas_H_Language` · `Sas_H_LanguageKey` · `Sas_H_LanguageText` | host | Dil anahtarları. Üretilen her ekranın EN+TR metinleri buraya (ve `LanguagesData.json`'a) yazılır. |
| **Data Source** | `App.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. **İki tür kayıt bir arada:** elle tanımlanan host/harici bağlantılar ve `Code = tenant adı` olan otomatik tenant bağlantıları (Tenants ekranındaki bağlantı diyaloğu yönetir, elle dokunma). `IsTenant` ekran önce tenant kaydını, yoksa `DataSourceCode`'u kullanır — çözüm kuralı `lowcode` §9.3.3. Bağlantı dizesi koda/JSON'a yazılmaz. |
| **Notifications › Types / Rules / Notifications** | `App.Notifications.NotificationTypes` · `.NotificationRules` · `.Notification` | `Sas_H_NotificationType` · `Sas_H_NotificationRule` · `Sas_H_Notification` | host | Olay bazlı bildirim (7 kanal); kural ekranındaki `CreateNotification` diyaloğu ile elle bildirim üretilir. Ayrıntı: §9.11. |
| **Background Workers › Recurring Jobs / Jobs** | `App.BackgroundWorkers.RecurringJobs` · `.Jobs` | `Sas_H_BackgroundWorker` · `Sas_H_BackgroundWorker_JobFlow` | host | Cron'lu işler ve çalıştırma geçmişi (`ExecutionDate`, `Duration`, `Status`, `FailureReason`, `ExceptionDetails`). Ayrıntı: §9.10 ve bu dosyanın §11'i. |
| **Background Workers › Recurring Jobs / Jobs** | `App.BackgroundWorkers.RecurringJobs` · `.Jobs` | `Sas_H_BackgroundWorker` · `Sas_H_BackgroundWorker_JobFlow` | host | Cron'lu işler ve çalıştırma geçmişi (`Tenant`, `ExecutionDate`, `Duration`, `Status`, `FailureReason`, `ExceptionDetails`). `Scope` alanı işin gövdesinin hangi bağlamda çalışacağını belirler: `Host` / `Tenant` / `Host ve Kiracılar`; her kapsam ayrı bir iş kaydıdır, `Tenant` kolonu hangi kiracıda çalıştığını gösterir. Ayrıntı: §9.10, `README.md` §14 ve bu dosyanın §11'i. |
| **Menus › Routes / Module / Menu / Manager** | `App.Menus.Routes` · `.Module` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Route` · `Sas_H_Module` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır, `Routes` yalnızca özel React sayfası eklenirken kullanılır. |
| **Developer Kit › SQL Query Manager** | `App.SqlQueryManager` · `/admin/sqlQueryManager` | — | — | View/procedure/function yazma ve çalıştırma; karar sırasının 2. adımı buradan başlar. |
| **Developer Kit › Custom Endpoints** | `App.DeveloperKit.CustomEndpoints` | `Sas_H_CustomEndpoint` | tenant | `Name`, `Url`, `Method`, `DataSourceCode`, `Sql`, `ParametersJson`, `PermissionsJson` — parametreli SQL'i yetkili bir HTTP ucuna çevirir. |
@ -1215,8 +1214,9 @@ 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. |
| **Bildirimler › Bildirim Türleri / Kuralları** | `App.Notifications.NotificationTypes` · `.NotificationRules` | `Adm_T_NotificationType` · `Adm_T_NotificationRule` | Olay bazlı bildirim (7 kanal); tür ve kural kiracıya aittir, yeni kiraciya varsayılan küme kopyalanır. Ayrıntı: §9.11, `README.md` §13.5.1. |
| **Bildirimler › Bildirimler** | `App.Notifications.Notification` | `Adm_T_Notification` | Hem kuyruk günlüğü hem elle gönderim ekranı: liste kural motorunun ürettiklerini de gösterir, ekleme formu alıcı × kanal açılımını kuyruğa yazar (`list-form-dynamic-api/notification-insert`). Ayrı bir manuel gönderim tablosu yoktur. Ayrıntı: `README.md` §13.5.1. |
| **Bildirimler › Bildirim Şablonları** | `App.Notifications.NotificationTemplates` | `Adm_T_NotificationTemplate` | Elle 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

@ -2838,7 +2838,9 @@ dosyalardan kurulur. Mekanik:
başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql`
`Hr_T_EmployeeAdvance.sql`'den önce gelir).
3. Dosya içeriği `GO` satırlarından batch'lere bölünür ve sırayla çalıştırılır. PostgreSQL'de
`GO` kullanılmaz.
`GO` kullanılmaz. Batch `ExecuteSqlRaw` ile gider ve metin `String.Format`'tan geçer; seeder
süslü parantezleri kaçışlar (`SqlDataSeeder.EscapeBraces`), yine de scriptte gereksiz `{}`
kullanma — JSON varsayılanı ya da regex niceleyici gerekiyorsa tek yerde tut.
4. **Hata seed'i durdurur.** Bir batch patlarsa istisna yukarı fırlatılır ve o kapsamın seed'i
yarım kalır. Bu yüzden her script tekrar tekrar çalıştırılabilir (idempotent) olmalıdır:
`CREATE OR ALTER` / `CREATE OR REPLACE` / `IF NOT EXISTS`.
@ -3423,8 +3425,10 @@ Ekranlar: `App.BackgroundWorkers.RecurringJobs` (tanımlar) ve `App.BackgroundWo
**Hangfire mekaniği** (`BackgroundWorkerInitializer` + `PlatformBackgroundWorker`) — tanımlar
Hangfire'a şöyle çevrilir:
- Aktif her kayıt bir **recurring job** olur: id `{WorkerType}:{Name}`, kuyruk `platform`,
zaman dilimi **Europe/Istanbul** — cron ifadeleri Türkiye saatine göre yazılır.
- Aktif her kayıt **kapsam başına** bir **recurring job** olur: host için id `{WorkerType}:{Name}`,
her kiracı için `{WorkerType}:{Name}#{TenantId}`; kuyruk `platform`, zaman dilimi
**Europe/Istanbul** — cron ifadeleri Türkiye saatine göre yazılır. Kiracı eklenip silindiğinde
liste yeniden üretilir (`BackgroundWorkerTenantHandler`).
- Yükleme sırasında `platform` kuyruğunda kalan ama artık bir kayda denk gelmeyen job'lar
**silinir**; yani iş listesinin tek kaynağı bu tablodur, Hangfire panelinden elle job açma.
- `MailQueueWorker` tanımı yüklenirken `Options.MailTemplate` Razor şablonu worker adıyla
@ -3432,7 +3436,8 @@ Hangfire'a şöyle çevrilir:
Her tetiklemede çalışma sırası:
1. `Name` ile **dağıtık kilit** alınır — alınamazsa (başka instance çalışıyor) sessizce çıkılır.
1. **Dağıtık kilit** alınır — host kapsamında `Name`, kiracı kapsamında `Name#{TenantId}` ile;
alınamazsa (başka instance aynı kapsamı çalıştırıyor) sessizce çıkılır.
2. `BeforeSp` doluysa `DataSourceCode` bağlantısında çağrılır.
3. Tipe göre gövde: `MailQueueWorker` → kuyruğu gönderir (§11 · reference) ·
`SqlWorker`/`BackupWorker` → SQL gövdesi (backup hedefi `App:BackupPath` ayarı) ·
@ -3440,8 +3445,14 @@ Her tetiklemede çalışma sırası:
`SessionCleanupWorker` → süresi geçen oturumları düşürür.
4. `AfterSp` doluysa çağrılır.
5. Hata `IExceptionNotifier`'a bildirilir ve loglanır — iş sessizce yutulmaz; çalıştırma
geçmişi `App.BackgroundWorkers.Jobs` ekranındadır (`ExecutionDate`, `Duration`, `Status`,
`FailureReason`, `ExceptionDetails`).
geçmişi `App.BackgroundWorkers.Jobs` ekranındadır (`Tenant`, `ExecutionDate`, `Duration`,
`Status`, `FailureReason`, `ExceptionDetails`) ve her satır tek bir kapsamın çalışmasıdır.
**Kapsam:** `Scope` alanı işin gövdesinin hangi bağlamda çalışacağını belirler —
`Host` (1) yalnızca host, `Tenant` (2) her kiracı için bir kez, `Host ve Kiracılar` (3) önce host
sonra kiracılar — her biri ayrı bir iş kaydı üretir, Jobs ekranındaki `Tenant` kolonu hangi
kiracıda çalıştığını gösterir. Kiracı tablosuna dokunan bir iş `Host` kapsamında çalışırsa kiracı satırlarını
göremez; kiracı verisi işleyen her işe kapsamını ver.
**Önemli:** `SqlWorker` ve `BackupWorker` tiplerinde asıl iş `BeforeSp`/`AfterSp`
prosedürlerindedir. Yani "her gece stok kapanışı" demek = `WorkerType: 2` +
@ -3457,7 +3468,7 @@ kurulur, kod yazılmaz:
| --- | --- |
| Zamanlanmış SQL işi (kapanış, özet tablo, temizlik) | `WorkerType: 2` + `BeforeSp` + `sql/object/` prosedürü |
| Zamanlanmış e-posta (rapor maili, hatırlatma) | `WorkerType: 1` + `Options` (Mail şablonu + `TableName`) + kuyruğa satır yazan `BeforeSp` |
| Olay bazlı bildirimlerin dağıtımı | `WorkerType: 3` (kuyruğu boşaltır) + Notification Type/Rule kayıtları (§9.11) |
| Olay bazlı bildirimlerin dağıtımı | `WorkerType: 3` + `Scope: 3` (kuyruğu host ve her kiracı için boşaltır) + Notification Type/Rule kayıtları (§9.11) |
| Oturum temizliği / yedekleme | `WorkerType: 4` / `5` (+ `App:BackupPath`) |
**Zamanlanmış e-posta (`MailQueueWorker`)** — "her sabah geciken siparişler listesi şu kişilere
@ -3479,8 +3490,12 @@ JSON'udur:
### 9.11 Bildirimler (`App.Notifications.*`)
Üç ekran: **Notification Types** (bildirim türü tanımı), **Notification Rules** (kim, hangi
türde, hangi kanaldan alacak), **Notifications** (üretilmiş bildirimler / geçmiş).
Dört ekran, tamamı **Administration › Bildirimler** altındadır: **Notification Types** (bildirim
türü tanımı), **Notification Rules** (kim, hangi türde, hangi kanaldan alacak), **Notifications**
(üretilmiş bildirimler / geçmiş **ve** elle gönderim formu), **Notification Templates** (mesaj
şablonları). Üç tablo da kiracı kapsamındadır (`Adm_T_NotificationType`,
`Adm_T_NotificationRule`, `Adm_T_Notification`); yeni kiracıya varsayılan tür ve kural kümesi
kopyalanır (`README.md` §13.5.1).
| `NotificationRule` alanı | Anlam |
| --- | --- |
@ -3495,6 +3510,11 @@ Bildirim üretme yolu: bir `Notification` kaydı oluşur (tür, kural, alıcı,
`NotificationWorker` (§9.10) kuyruğu kanala göre gönderir. Kayıt SQL tarafından
(`InsertAfterCommand`, prosedür) ya da Dynamic Service'ten yazılabilir.
Kural yazmadan gönderim istendiğinde ayrı bir ekran/tablo açılmaz: **Notifications** ekranının
ekleme formu alıcı (`UserId`) ve kanal (`NotificationChannel`) çoklu seçimini alır,
`list-form-dynamic-api/notification-insert` ucu her alıcı × kanal açılımı için bir satır yazar.
Bildirim tipi `Manuel`'dir; kuyruğu yine `NotificationWorker` boşaltır.
Onay akışındaki **`Inform` düğümü e-posta gönderir** (MailQueue üzerinden); UI bildirimi
istiyorsan ayrıca bir bildirim kuralı gerekir.

View file

@ -1208,19 +1208,31 @@ 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)
### 13.5.1. Bildirim ekranı ve elle 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.
Bildirimin tamamı Administration menüsünde `App.Administration.Notifications` grubundadır:
**Bildirim Türleri**, **Bildirim Kuralları**, **Bildirimler**, **Bildirim Şablonları**. Grup
Agenda'nın üstündedir; Saas altında bildirim ekranı **yoktur**.
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.
Tablolar kiracı kapsamındadır (`Adm_T_NotificationType`, `Adm_T_NotificationRule`,
`Adm_T_Notification`; üçü de `IMultiTenant`). Her kiracı kendi türünü, kuralını ve kuyruğunu
görür; host kaydı (`TenantId = null`) yalnızca host oturumunda görünür. Yeni bir kiracı
oluşturulduğunda varsayılan tür ve kural kümesi o kiracıya kopyalanır
(`NotificationTenantDefaultsSeedContributor`): kaynak host kapsamındaki
`data/App.Notifications.Notification{Types,Rules}.json` dosyalarıdır, satırlar **yeni
kimliklerle** yazılır ve eşleşme ad üzerinden yapıldığı için ikinci çalıştırma bir şey eklemez.
**Bildirimler** ekranı (`App.Notifications.Notification`) hem kuyruk günlüğü hem gönderim
ekranıdır: liste kural motorunun ürettiklerini de gösterir, ekleme formu ise elle gönderimdir.
Ayrı bir manuel gönderim kaydı yoktur — gönderimin kaydı bildirimin kendisidir. Ekleme anında
yazma işi `list-form-dynamic-api/notification-insert` ucuna gider (`InsertServiceAddress`); uç
seçilen alıcı × kanal açılımını `INotificationManager.SendManualNotificationAsync` ile kuyruğa
yazar — 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.
Formdaki `NotificationChannel` ve `UserId` alanları listede tek değerlidir (her satır bir
alıcı × kanal), formda `dxTagBox` ile çoklu seçilir; tipli DTO yolunda çoklu değeri
`MultiValueStringJsonConverter` taşır (`CreateNotificationInput`).
| Kural | Nerede |
| --- | --- |
@ -1228,7 +1240,8 @@ gelmesi. Güncelleme kapalıdır, silme kaydı kaldırır.
| `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) |
| İlgili kanalda adresi olmayan alıcı atlanır | Hiç satır üretilemezse uç `App.Notifications.SkippedChannel` gerekçesiyle hata döndürür; kısmi atlamada gönderim sürer |
| Metnin geldiği şablon kayda yazılır | `Notification.NotificationTemplateId` (provenans; tür değil, metin kaynağı) |
| 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ı`,
@ -1243,9 +1256,9 @@ Gövdedeki yer tutucular alıcı bazında çözülür (`NotificationMessageToken
`{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).
**Mail konu satırı:** bildirim kaydı `Subject` kolonu taşır (`Adm_T_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
(`Platform Bildirim: {tip}`). Kural motorundan gelen bildirimlerde kolon boş kalır, elle
gönderimde ekrandan ya da şablondan gelir — yani konu artık gövdeye gömülmez.
### 13.6. Göndericiler (`Sozsoft.Sender`)
@ -1455,6 +1468,20 @@ ekranlarından yapılır; yetkiler `App.BackgroundWorkers.RecurringJobs` ve `.Jo
| `SessionCleanupWorker` | Süresi dolmuş oturumları temizler. |
| `BackupWorker` | Veritabanı yedeği alır ve eski yedekleri temizler. |
**Kapsam (`Scope`).** İş kaydı her zaman host tablosundadır (`Sas_H_BackgroundWorker`); işin
**gövdesinin** hangi kiracı bağlamında kaç kez çalışacağını `Scope` belirler
(`WorkerScopeEnum`): `Host` yalnızca host bağlamında bir kez, `Tenant` her kiracı için bir kez,
`All` önce host sonra her kiracı için. **Her kapsam ayrı bir Hangfire işidir**
(`BackgroundWorkerInitializer`): host `{WorkerType}:{Name}`, kiracı `{WorkerType}:{Name}#{TenantId}`.
`PlatformBackgroundWorker.StartAsync` tek kapsam çalıştırır, kilidi de kapsam başına alır — bir
kiracının hatası ya da yavaşlığı diğerlerini durdurmaz ve Jobs ekranı kiracı başına süre, durum ve
hata gösterir (`Tenant` kolonu; ad `Sas_H_BackgroundWorker_JobFlow` içinde iş adının `#` ekinden
`AbpTenants`'a çözülür). Kiracı adı sonradan değişebildiği için iş adına kimlik yazılır.
Kiracı eklenip silindiğinde zamanlama yeniden üretilir (`BackgroundWorkerTenantHandler`).
`BeforeSp`/`AfterSp` de kapsam başına çalışır, çünkü dokundukları veri kiracıya aittir.
Bildirim kuyruğu kiracı tablosunda olduğu için `NotificationWorker` işi `All` kapsamıyla gelir
(`data/App.BackgroundWorkers.RecurringJobs.json`).
Hangfire panosu geliştirmede serbest, üretimde `AbpHangfireAuthorizationFilter` ile korumalıdır
(`/hangfire`).

View file

@ -1,15 +1,25 @@
using System;
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Notifications.Entities;
public class Notification : FullAuditedEntity<Guid>
public class Notification : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid NotificationRuleId { get; set; }
public required string NotificationChannel { get; set; }
public required Guid NotificationTypeId { get; set; }
public virtual NotificationType NotificationType { get; set; }
/// <summary>
/// Metni hangi sablondan geldigi; elle gonderimde secilirse yazilir, kural motorundan gecen
/// bildirimlerde bostur. Navigasyon yoktur: sablon uygulamanin entity'sidir
/// (<c>Adm_T_NotificationTemplate</c>), bildirim modulu ona referans veremez.
/// </summary>
public Guid? NotificationTemplateId { get; set; }
// Bildirim tipine göre bildirimi alacak olan hedef bilgisini içerir
public required string Identifier { get; set; } //UserId, Email, PhoneNumber..
public Guid? UserId { get; set; }
@ -28,4 +38,3 @@ public class Notification : FullAuditedEntity<Guid>
public virtual NotificationRule NotificationRule { get; set; }
}

View file

@ -1,11 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Notifications.Entities;
public class NotificationRule : FullAuditedEntity<Guid>
public class NotificationRule : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public required Guid NotificationTypeId { get; set; }
public NotificationType NotificationType { get; set; }

View file

@ -1,11 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Notifications.Entities;
public class NotificationType : FullAuditedEntity<Guid>
public class NotificationType : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public required string Name { get; set; }
/// <summary>

View file

@ -1,9 +1,15 @@
namespace Sozsoft.Notifications.Domain;
namespace Sozsoft.Notifications.Domain;
public static class Prefix
{
public static string MenuPrefix { get; set; } = "Sas";
public static string HostPrefix { get; set; } = "H";
public static string MenuPrefix { get; set; } = "Adm";
/// <summary>
/// Bildirim tablolari kiraci kapsamindadir: tip, kural ve kuyruk kayitlarinin her biri bir
/// kiraciya aittir, host kaydi yoktur. Bu yuzden prefix host (<c>H</c>) degil tenant (<c>T</c>).
/// </summary>
public static string TenantPrefix { get; set; } = "T";
public static string? DbSchema { get; set; } = null;
public const string ConnectionStringName = "Notifications";
@ -13,6 +19,6 @@ public static class TablePrefix
{
public static string ByName(string tableName)
{
return $"{Prefix.MenuPrefix}_{Prefix.HostPrefix}_{tableName}";
return $"{Prefix.MenuPrefix}_{Prefix.TenantPrefix}_{tableName}";
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -34,6 +34,9 @@ public interface INotificationManager : ITransientDependency
public class ManualNotificationRequest
{
public Guid NotificationTypeId { get; set; }
/// <summary>Metnin geldigi sablon; secilmediyse bos. Uretilen her satira provenans olarak yazilir.</summary>
public Guid? NotificationTemplateId { get; set; }
public IReadOnlyCollection<Guid> UserIds { get; set; } = [];
public IReadOnlyCollection<string> Channels { get; set; } = [];
public string Message { get; set; } = string.Empty;
@ -49,6 +52,9 @@ public class ManualNotificationRequest
public class ManualNotificationResult
{
public int SentCount { get; set; }
/// <summary>Uretilen ilk kaydin kimligi; ekran gonderimden sonra bu satira konumlanir.</summary>
public Guid FirstNotificationId { get; set; }
public Dictionary<string, int> SkippedByChannel { get; set; } = [];
public int SkippedCount => SkippedByChannel.Values.Sum();
}
@ -184,7 +190,7 @@ public class NotificationManager : INotificationManager
return result;
}
// Sas_H_Notification.NotificationRuleId NOT NULL'dur. Manuel gonderim kural motorundan
// Adm_T_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
@ -216,6 +222,7 @@ public class NotificationManager : INotificationManager
notifications.Add(new Notification
{
NotificationTypeId = request.NotificationTypeId,
NotificationTemplateId = request.NotificationTemplateId,
NotificationRuleId = anchorRule.Id,
NotificationChannel = channel,
Identifier = identifier!,
@ -230,6 +237,7 @@ public class NotificationManager : INotificationManager
result.SentCount = notifications.Count;
await SaveAndAnnounceAsync(notifications);
result.FirstNotificationId = notifications.Count == 0 ? Guid.Empty : notifications[0].Id;
return result;
}

View file

@ -1,4 +1,4 @@

using Microsoft.EntityFrameworkCore;
using Sozsoft.Notifications.Domain;
using Sozsoft.Notifications.Entities;
@ -21,6 +21,9 @@ public static class NotificationDbContextModelCreatingExtensions
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.Description).HasMaxLength(2048);
// Tur adi kiraci icinde tekildir: kural ve tetikleyici kod turu adiyla bulur.
b.HasIndex(x => new { x.TenantId, x.Name });
});
builder.Entity<NotificationRule>(b =>
@ -34,6 +37,8 @@ public static class NotificationDbContextModelCreatingExtensions
b.Property(x => x.RecipientId).HasMaxLength(256);
b.Property(x => x.Channel).IsRequired().HasMaxLength(64);
b.HasIndex(x => new { x.TenantId, x.NotificationTypeId });
b.HasOne(x => x.NotificationType)
.WithMany(x => x.NotificationRules)
.HasForeignKey(x => x.NotificationTypeId)
@ -53,6 +58,10 @@ public static class NotificationDbContextModelCreatingExtensions
b.Property(x => x.Message).IsRequired().HasMaxLength(2048);
b.Property(x => x.Subject).HasMaxLength(256);
// Kuyrugu bosaltan worker kiraci kiraci calisir ve her turunde yalnizca gonderilmemis
// kayitlari okur; kanal ayrimini bellekte degil indeksten yapar.
b.HasIndex(x => new { x.TenantId, x.IsSent, x.NotificationChannel });
b.HasOne(x => x.NotificationType)
.WithMany(x => x.Notifications)
.HasForeignKey(x => x.NotificationTypeId)

View file

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Sozsoft.Platform.ListForms.DynamicApi;
/// <summary>
/// Bildirim ekranindan elle yapilan gonderimin girdisi. Alan adlari ekranin kolonlariyla
/// birebir aynidir; <see cref="NotificationChannel"/> ve <see cref="UserId"/> ekranda
/// <c>dxTagBox</c> ile coklu secilir, uc bunlari alici x kanal acilimina cevirir ve her acilim
/// icin bir bildirim satiri yazar. Tipli DTO yolunda <c>QueryHelper</c> devreye girmedigi icin
/// dizi/metin donusumu converter ile yapilir.
/// </summary>
public class CreateNotificationInput
{
public string NotificationTemplateId { get; set; }
[JsonConverter(typeof(MultiValueStringJsonConverter))]
public string NotificationChannel { get; set; }
[JsonConverter(typeof(MultiValueStringJsonConverter))]
public string UserId { get; set; }
public string Subject { get; set; }
public string Message { get; set; }
}

View file

@ -1,23 +0,0 @@
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,5 +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);
Task<Guid> PostNotificationInsertAsync(DynamicApiBaseInput<CreateNotificationInput> input);
}

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;
@ -11,6 +13,7 @@ using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;
namespace Sozsoft.Platform;
@ -18,17 +21,20 @@ namespace Sozsoft.Platform;
public class BackgroundWorkerInitializer : ITransientDependency
{
private readonly IRepository<BackgroundWorker, Guid> _repo;
private readonly ITenantRepository _tenantRepository;
private readonly PlatformTemplateManager _templateManager;
private readonly ILogger<BackgroundWorkerInitializer> _logger;
private readonly IUnitOfWorkManager _unitOfWorkManager;
public BackgroundWorkerInitializer(
IRepository<BackgroundWorker, Guid> repo,
ITenantRepository tenantRepository,
PlatformTemplateManager templateManager,
ILogger<BackgroundWorkerInitializer> logger,
IUnitOfWorkManager unitOfWorkManager)
{
_repo = repo;
_tenantRepository = tenantRepository;
_templateManager = templateManager;
_logger = logger;
_unitOfWorkManager = unitOfWorkManager;
@ -46,33 +52,49 @@ public class BackgroundWorkerInitializer : ITransientDependency
var jobs = JobStorage.Current.GetConnection().GetRecurringJobs();
var workers = await _repo.GetListAsync(a => a.IsActive);
var tenantIds = (await _tenantRepository.GetListAsync())
.Select(tenant => (Guid?)tenant.Id)
.ToList();
_logger.LogInformation(
"Loading {WorkerCount} active background workers into Hangfire.",
workers.Count);
// Her kapsam (host ve her kiraci) ayri bir isdir: is kaydi hangi kiracida ne kadar
// surdugunu ve nerede hata aldigini tasisin diye.
var schedule = workers
.SelectMany(worker => ResolveScopeTenants(worker.Scope, tenantIds)
.Select(tenantId => (Worker: worker, TenantId: tenantId, JobId: BuildJobId(worker, tenantId))))
.ToList();
var scheduledJobIds = schedule.Select(item => item.JobId).ToHashSet();
foreach (var job in jobs)
{
if (job.Queue == "platform" && !workers.Exists(a => $"{a.WorkerType}:{a.Name}" == job.Id))
if (job.Queue == "platform" && !scheduledJobIds.Contains(job.Id))
{
RecurringJob.RemoveIfExists(job.Id);
}
}
foreach (var worker in workers)
foreach (var (worker, tenantId, jobId) in schedule)
{
var workerId = worker.Id;
var scopeTenantId = tenantId;
RecurringJob.AddOrUpdate<IPlatformBackgroundWorker>(
$"{worker.WorkerType}:{worker.Name}",
jobId,
"platform",
(w) => w.StartAsync(workerId, default),
(w) => w.StartAsync(workerId, scopeTenantId, default),
worker.Cron,
new RecurringJobOptions
{
TimeZone = turkeyTimeZone
});
}
foreach (var worker in workers)
{
if (worker.WorkerType == WorkerTypeEnum.MailQueueWorker && !worker.Options.IsNullOrWhiteSpace())
{
var options = JsonSerializer.Deserialize<MailQueueWorkerOptions>(worker.Options);
@ -91,6 +113,30 @@ public class BackgroundWorkerInitializer : ITransientDependency
await uow.CompleteAsync();
}
/// <summary>
/// Isin adi: host kapsami sade kalir, kiraci kapsami kiraci kimligiyle ayrisir. Kiraci adi
/// degisebildigi icin ad degil kimlik kullanilir; ekran adi <c>AbpTenants</c>'tan cozer.
/// </summary>
private static string BuildJobId(BackgroundWorker worker, Guid? tenantId)
{
var name = $"{worker.WorkerType}:{worker.Name}";
return tenantId is null ? name : $"{name}#{tenantId}";
}
/// <summary>Kapsamin karsiligi olan kiraci listesi: host icin tek bir <c>null</c>.</summary>
private static IEnumerable<Guid?> ResolveScopeTenants(WorkerScopeEnum scope, IReadOnlyList<Guid?> tenantIds)
{
if (scope == WorkerScopeEnum.Host)
{
return [null];
}
return scope == WorkerScopeEnum.All
? tenantIds.Prepend(null)
: tenantIds;
}
private static TimeZoneInfo GetTurkeyTimeZone()
{
var timeZoneIds = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

View file

@ -0,0 +1,49 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.TenantManagement;
namespace Sozsoft.Platform;
/// <summary>
/// Kiraci kapsamli isler kiraci basina ayri bir Hangfire isi olarak kuruldugu icin kiraci
/// eklenip silindiginde zamanlama yeniden uretilir; aksi halde yeni kiraci bir sonraki
/// uygulama baslangicina kadar hic calismaz.
/// </summary>
public class BackgroundWorkerTenantHandler :
ILocalEventHandler<EntityCreatedEventData<Tenant>>,
ILocalEventHandler<EntityDeletedEventData<Tenant>>,
ITransientDependency
{
private readonly BackgroundWorkerInitializer _initializer;
private readonly ILogger<BackgroundWorkerTenantHandler> _logger;
public BackgroundWorkerTenantHandler(
BackgroundWorkerInitializer initializer,
ILogger<BackgroundWorkerTenantHandler> logger)
{
_initializer = initializer;
_logger = logger;
}
public Task HandleEventAsync(EntityCreatedEventData<Tenant> eventData)
{
return RescheduleAsync(eventData.Entity);
}
public Task HandleEventAsync(EntityDeletedEventData<Tenant> eventData)
{
return RescheduleAsync(eventData.Entity);
}
private async Task RescheduleAsync(Tenant tenant)
{
_logger.LogInformation(
"Rescheduling background workers because tenant {TenantName} changed.",
tenant.Name);
await _initializer.RunAsync();
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -38,7 +38,6 @@ 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;
@ -53,7 +52,6 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
IOptions<IdentityOptions> identityOptions,
IRepository<Order, Guid> orderRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer,
IRepository<NotificationManual, Guid> notificationManualRepository,
IRepository<NotificationTemplate, Guid> notificationTemplateRepository,
IReadOnlyRepository<Sozsoft.Notifications.Entities.NotificationType> notificationTypeRepository,
INotificationManager notificationManager)
@ -67,7 +65,6 @@ 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;
@ -397,18 +394,19 @@ 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
/// Bildirim ekranindan elle gonderim. Secilen her alici x kanal acilimi icin bir bildirim
/// satiri yazilir; kaydin kendisi gonderimin gunlugudur, kanaldan cikisi
/// <c>NotificationWorker</c> yapar. Gonderim yolu kural motorununkiyle 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)
[Authorize(PlatformConsts.AppCodes.Notifications.NotificationCommands.Create)]
public async Task<Guid> PostNotificationInsertAsync(DynamicApiBaseInput<CreateNotificationInput> input)
{
var data = input.Data ?? throw new UserFriendlyException(L["App.Platform.Error"]);
if (data.Channels.IsNullOrWhiteSpace() || data.UserIds.IsNullOrWhiteSpace())
if (data.NotificationChannel.IsNullOrWhiteSpace() || data.UserId.IsNullOrWhiteSpace())
{
throw new UserFriendlyException(L["App.ManualNotification.RecipientAndChannelRequired"]);
throw new UserFriendlyException(L["App.Notifications.RecipientAndChannelRequired"]);
}
var templateId = ParseGuid(data.NotificationTemplateId);
@ -426,40 +424,30 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
if (message.IsNullOrWhiteSpace())
{
throw new UserFriendlyException(L["App.ManualNotification.MessageRequired"]);
throw new UserFriendlyException(L["App.Notifications.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);
?? throw new UserFriendlyException(L["App.Notifications.TypeNotFound"]);
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)],
NotificationTemplateId = templateId == Guid.Empty ? null : templateId,
Channels = SplitMultiValue(data.NotificationChannel),
UserIds = [.. SplitMultiValue(data.UserId).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);
// Hic satir uretilmediyse gonderim sessizce kaybolmasin: secilen alicilarin hicbirinde o
// kanalin adresi yok demektir ve cozum kullanici kaydindadir.
if (result.SentCount == 0)
{
throw new UserFriendlyException(BuildSkippedNote(result) ?? L["App.Notifications.NoRecipientResolved"]);
}
return record.Id;
return result.FirstNotificationId;
}
private static List<string> SplitMultiValue(string value)
@ -478,6 +466,6 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
? null
: string.Join(" | ", result.SkippedByChannel
.Where(item => item.Value > 0)
.Select(item => L["App.ManualNotification.SkippedChannel", item.Key, item.Value].Value));
.Select(item => L["App.Notifications.SkippedChannel", item.Key, item.Value].Value));
}
}

View file

@ -2844,6 +2844,12 @@
"en": "Success. Please confirm your account by clicking the link.",
"tr": "Başarılı. Lütfen bağlantıya tıklayarak hesabınızı onaylayın."
},
{
"resourceName": "Platform",
"key": "App.IdentityManagement",
"en": "Identity Management",
"tr": "Kimlik Yönetimi"
},
{
"resourceName": "Platform",
"key": "Abp.Identity",
@ -13776,6 +13782,12 @@
"en": "Job Name",
"tr": "İş Adı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Tenant",
"en": "Tenant",
"tr": "Kiracı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ExecutionDate",
@ -28149,20 +28161,20 @@
{
"resourceName": "Platform",
"key": "App.Notifications.NotificationTypes.Description",
"en": "Notification Types — the event definitions that produce notifications. Table: Sas_H_NotificationType · host scope · list screen.\nUsage: one row per event that should notify someone — NewOrder, OrderApproved, StockBelowCritical, NewUser, ApprovalPending. The type is the joint that binds the event to the rules: the rule screen selects a type and the code raising the notification passes the same name.\nFields:\n• Name → the type code referenced from everywhere. Write it stable and descriptive; it is used from SQL and from services\n• Description → what the type is for: which event raises it and who is expected to act. It is shown as a hint on the user notification settings screen, so write it for the reader, at length if needed\nHow it is triggered: NotificationManager.SendNotificationAsync(typeId, message) reads every rule of the type and produces one notification row per recipient. The trigger may be a screen's InsertAfterCommand/UpdateAfterCommand, a stored procedure, a Dynamic Service, or the 'Create' dialog on the Notification Rules screen.\nOrder of setup: 1) define the type here, 2) define the recipient and channel on Notification Rules, 3) make sure the NotificationWorker job is active.\nCaution: renaming a type breaks every caller referencing the old name — add a new type instead. A type with no rule produces no notification, it fails silently.",
"tr": "Bildirim Türleri — bildirim üreten olay tanımları. Tablo: Sas_H_NotificationType · host kapsamlı · liste ekranı.\nKullanım: birine haber verilmesi gereken her olay için bir satır — YeniSiparis, SiparisOnaylandi, StokKritikAltinda, YeniKullanici, OnayBekliyor. Tür, olayı kurallara bağlayan eklem yeridir: kural ekranı bir tür seçer, bildirimi tetikleyen kod da aynı adı geçirir.\nAlanlar:\n• Name → her yerden referans verilen tür kodu. Kalıcı ve açıklayıcı yaz; SQL'den ve servislerden kullanılır\n• Description → türün ne işe yaradığı: hangi olayda tetiklendiği ve kimin ne yapması beklendiği. Kullanıcının bildirim tercihleri ekranında ipucu olarak görünür, gerekirse uzun uzun yaz\nNasıl tetiklenir: NotificationManager.SendNotificationAsync(typeId, mesaj) çağrısı türe ait tüm kuralları okur ve her alıcı için bir bildirim satırı üretir. Tetikleyici; ekranın InsertAfterCommand/UpdateAfterCommand tanımı, saklı yordam, Dynamic Service veya Bildirim Kuralları ekranındaki 'Create' diyaloğu olabilir.\nKurulum sırası: 1) türü burada tanımla, 2) Bildirim Kuralları ekranında alıcı ve kanalı tanımla, 3) NotificationWorker işinin aktif olduğundan emin ol.\nDikkat: türün adını değiştirmek eski adı referans veren tüm çağıranları bozar — bunun yerine yeni tür ekle. Kuralı olmayan tür bildirim üretmez, sessizce çalışır."
"en": "Notification Types — the event definitions that produce notifications. Table: Adm_T_NotificationType · tenant scope · list screen.\nUsage: one row per event that should notify someone — NewOrder, OrderApproved, StockBelowCritical, NewUser, ApprovalPending. The type is the joint that binds the event to the rules: the rule screen selects a type and the code raising the notification passes the same name.\nFields:\n• Name → the type code referenced from everywhere. Write it stable and descriptive; it is used from SQL and from services\n• Description → what the type is for: which event raises it and who is expected to act. It is shown as a hint on the user notification settings screen, so write it for the reader, at length if needed\nHow it is triggered: NotificationManager.SendNotificationAsync(typeId, message) reads every rule of the type and produces one notification row per recipient. The trigger may be a screen's InsertAfterCommand/UpdateAfterCommand, a stored procedure, a Dynamic Service, or the 'Create' dialog on the Notification Rules screen.\nOrder of setup: 1) define the type here, 2) define the recipient and channel on Notification Rules, 3) make sure the NotificationWorker job is active.\nCaution: renaming a type breaks every caller referencing the old name — add a new type instead. A type with no rule produces no notification, it fails silently.",
"tr": "Bildirim Türleri — bildirim üreten olay tanımları. Tablo: Adm_T_NotificationType · kiracı kapsamlı · liste ekranı.\nKullanım: birine haber verilmesi gereken her olay için bir satır — YeniSiparis, SiparisOnaylandi, StokKritikAltinda, YeniKullanici, OnayBekliyor. Tür, olayı kurallara bağlayan eklem yeridir: kural ekranı bir tür seçer, bildirimi tetikleyen kod da aynı adı geçirir.\nAlanlar:\n• Name → her yerden referans verilen tür kodu. Kalıcı ve açıklayıcı yaz; SQL'den ve servislerden kullanılır\n• Description → türün ne işe yaradığı: hangi olayda tetiklendiği ve kimin ne yapması beklendiği. Kullanıcının bildirim tercihleri ekranında ipucu olarak görünür, gerekirse uzun uzun yaz\nNasıl tetiklenir: NotificationManager.SendNotificationAsync(typeId, mesaj) çağrısı türe ait tüm kuralları okur ve her alıcı için bir bildirim satırı üretir. Tetikleyici; ekranın InsertAfterCommand/UpdateAfterCommand tanımı, saklı yordam, Dynamic Service veya Bildirim Kuralları ekranındaki 'Create' diyaloğu olabilir.\nKurulum sırası: 1) türü burada tanımla, 2) Bildirim Kuralları ekranında alıcı ve kanalı tanımla, 3) NotificationWorker işinin aktif olduğundan emin ol.\nDikkat: türün adını değiştirmek eski adı referans veren tüm çağıranları bozar — bunun yerine yeni tür ekle. Kuralı olmayan tür bildirim üretmez, sessizce çalışır."
},
{
"resourceName": "Platform",
"key": "App.Notifications.NotificationRules.Description",
"en": "Notification Rules — who receives which event, through which channel. Table: Sas_H_NotificationRule · host scope · list screen. The 'Create' row command produces a notification by hand from the selected rule (CreateNotification dialog).\nFields:\n• NotificationTypeId → which event; the lookup comes from the Notification Types screen\n• RecipientType + RecipientId → who receives it (see below)\n• Channel → through which channel it is delivered (see below)\n• IsActive → is the rule on\n• IsFixed → the user cannot turn it off from the profile; mandatory notification\n• IsCustomized → the user has written their own preference for this type/channel\nRecipientType — who the rule addresses:\n• All → every active user\n• User → RecipientId is the user name (AbpUsers.UserName)\n• Role → RecipientId is the role name; everyone holding the role receives it\n• OrganizationUnit → RecipientId is the unit code; the unit and all of its children receive it\n• Custom → RecipientId is the address itself (phone, e-mail, chat id); no user lookup is made, so it can also notify someone outside the system\nChannel setup — for every channel except Custom the address is resolved from the user record:\n• Mail → AbpUsers.Email. Sender settings: Abp.Mailing.* and App.Sender.Mail.*\n• Sms → AbpUsers.PhoneNumber. Settings: App.Sender.Sms.PostaGuvercini.* (Url, Username, Password)\n• WhatsApp → AbpUsers.PhoneNumber, normalized to +90…. Settings: App.Sender.WhatsApp.Url, PhoneNumberId, Token, TemplateName. The message is sent through an approved template\n• Telegram → AbpUsers.TelegramChatId. Settings: App.Sender.Telegram.Url, BotToken, ParseMode. How to obtain the chat id: the user sends /start to the bot once, then the id is read from the result[].message.chat.id field of https://api.telegram.org/bot<BotToken>/getUpdates and written into the Telegram Chat Id field of Identity › Users (the user sees it read-only on their profile). Without /start the send fails with 'chat not found'\n• Rocket → the user's RocketUsername. Settings: App.Sender.Rocket.Url, UserId, Token\n• Desktop / UiActivity / UiToast → the user id itself; delivered inside the application (activity log, toast, browser notification)\nFlow: the rule produces a Notification row, then the NotificationWorker (Background Workers › Recurring Jobs, WorkerType = NotificationWorker, cron */5 by default) empties the queue channel by channel.\nCaution: a user preference (IsCustomized with RecipientType = User) is not a rule: it only switches its own owner off for that type and channel, and an IsFixed rule ignores it. Rules themselves decide the recipients, and when several rules address the same user only one record is produced per channel. A rule whose channel address is empty on the user is skipped silently — when a message does not arrive, look at the Notifications screen first: an empty Identifier means a missing address, IsSent = false means a failing send.",
"tr": "Bildirim Kuralları — hangi olayı kim, hangi kanaldan alacak. Tablo: Sas_H_NotificationRule · host kapsamlı · liste ekranı. Satırdaki 'Create' komutu seçili kuraldan elle bildirim üretir (CreateNotification diyaloğu).\nAlanlar:\n• NotificationTypeId → hangi olay; lookup Bildirim Türleri ekranından gelir\n• RecipientType + RecipientId → kimin alacağı (aşağıya bak)\n• Channel → hangi kanaldan iletileceği (aşağıya bak)\n• IsActive → kural açık mı\n• IsFixed → kullanıcı profilinden kapatamaz; zorunlu bildirim\n• IsCustomized → kullanıcı bu tür/kanal için kendi tercihini yazmış\nRecipientType — kural kimi hedefler:\n• All → tüm aktif kullanıcılar\n• User → RecipientId kullanıcı adıdır (AbpUsers.UserName)\n• Role → RecipientId rol adıdır; role sahip herkes alır\n• OrganizationUnit → RecipientId birim kodudur; birim ve tüm alt birimleri alır\n• Custom → RecipientId doğrudan adresin kendisidir (telefon, e-posta, chat id); kullanıcı araması yapılmaz, böylece sistem dışındaki birine de haber verilebilir\nKanal kurulumu — Custom dışındaki her kanalda adres kullanıcı kaydından çözülür:\n• Mail → AbpUsers.Email. Gönderim ayarları: Abp.Mailing.* ve App.Sender.Mail.*\n• Sms → AbpUsers.PhoneNumber. Ayarlar: App.Sender.Sms.PostaGuvercini.* (Url, Username, Password)\n• WhatsApp → AbpUsers.PhoneNumber, +90… biçimine normalize edilir. Ayarlar: App.Sender.WhatsApp.Url, PhoneNumberId, Token, TemplateName. Mesaj onaylı şablon üzerinden gider\n• Telegram → AbpUsers.TelegramChatId. Ayarlar: App.Sender.Telegram.Url, BotToken, ParseMode. Chat id nasıl alınır: kullanıcı bota bir kez /start yazar, ardından https://api.telegram.org/bot<BotToken>/getUpdates çıktısındaki result[].message.chat.id değeri okunur ve Identity › Kullanıcılar ekranındaki Telegram Chat Id alanına yazılır (kullanıcı kendi profilinde salt okunur görür). /start yazılmadan gönderim 'chat not found' hatası verir\n• Rocket → kullanıcının RocketUsername alanı. Ayarlar: App.Sender.Rocket.Url, UserId, Token\n• Desktop / UiActivity / UiToast → kullanıcının kendi kimliği; uygulama içinde iletilir (aktivite kaydı, toast, tarayıcı bildirimi)\nAkış: kural bir Notification satırı üretir, ardından NotificationWorker (Background Workers › Recurring Jobs, WorkerType = NotificationWorker, varsayılan cron */5) kuyruğu kanal kanal boşaltır.\nDikkat: kullanıcı tercihi (IsCustomized + RecipientType = User) bir kural değildir: yalnızca kendi sahibini o tür ve kanalda kapatır, IsFixed kuralda ise dikkate alınmaz. Alıcıları kurallar belirler; aynı kullanıcıyı birden çok kural hedeflerse kanal başına tek kayıt üretilir. Kanal adresi kullanıcıda boş olan kural sessizce atlanır — mesaj gitmediğinde önce Bildirimler ekranına bak: Identifier boşsa adres eksiktir, IsSent = false ise gönderim hata alıyordur."
"en": "Notification Rules — who receives which event, through which channel. Table: Adm_T_NotificationRule · tenant scope · list screen. The 'Create' row command produces a notification by hand from the selected rule (CreateNotification dialog).\nFields:\n• NotificationTypeId → which event; the lookup comes from the Notification Types screen\n• RecipientType + RecipientId → who receives it (see below)\n• Channel → through which channel it is delivered (see below)\n• IsActive → is the rule on\n• IsFixed → the user cannot turn it off from the profile; mandatory notification\n• IsCustomized → the user has written their own preference for this type/channel\nRecipientType — who the rule addresses:\n• All → every active user\n• User → RecipientId is the user name (AbpUsers.UserName)\n• Role → RecipientId is the role name; everyone holding the role receives it\n• OrganizationUnit → RecipientId is the unit code; the unit and all of its children receive it\n• Custom → RecipientId is the address itself (phone, e-mail, chat id); no user lookup is made, so it can also notify someone outside the system\nChannel setup — for every channel except Custom the address is resolved from the user record:\n• Mail → AbpUsers.Email. Sender settings: Abp.Mailing.* and App.Sender.Mail.*\n• Sms → AbpUsers.PhoneNumber. Settings: App.Sender.Sms.PostaGuvercini.* (Url, Username, Password)\n• WhatsApp → AbpUsers.PhoneNumber, normalized to +90…. Settings: App.Sender.WhatsApp.Url, PhoneNumberId, Token, TemplateName. The message is sent through an approved template\n• Telegram → AbpUsers.TelegramChatId. Settings: App.Sender.Telegram.Url, BotToken, ParseMode. How to obtain the chat id: the user sends /start to the bot once, then the id is read from the result[].message.chat.id field of https://api.telegram.org/bot<BotToken>/getUpdates and written into the Telegram Chat Id field of Identity › Users (the user sees it read-only on their profile). Without /start the send fails with 'chat not found'\n• Rocket → the user's RocketUsername. Settings: App.Sender.Rocket.Url, UserId, Token\n• Desktop / UiActivity / UiToast → the user id itself; delivered inside the application (activity log, toast, browser notification)\nFlow: the rule produces a Notification row, then the NotificationWorker (Background Workers › Recurring Jobs, WorkerType = NotificationWorker, cron */5 by default) empties the queue channel by channel.\nCaution: a user preference (IsCustomized with RecipientType = User) is not a rule: it only switches its own owner off for that type and channel, and an IsFixed rule ignores it. Rules themselves decide the recipients, and when several rules address the same user only one record is produced per channel. A rule whose channel address is empty on the user is skipped silently — when a message does not arrive, look at the Notifications screen first: an empty Identifier means a missing address, IsSent = false means a failing send.",
"tr": "Bildirim Kuralları — hangi olayı kim, hangi kanaldan alacak. Tablo: Adm_T_NotificationRule · kiracı kapsamlı · liste ekranı. Satırdaki 'Create' komutu seçili kuraldan elle bildirim üretir (CreateNotification diyaloğu).\nAlanlar:\n• NotificationTypeId → hangi olay; lookup Bildirim Türleri ekranından gelir\n• RecipientType + RecipientId → kimin alacağı (aşağıya bak)\n• Channel → hangi kanaldan iletileceği (aşağıya bak)\n• IsActive → kural açık mı\n• IsFixed → kullanıcı profilinden kapatamaz; zorunlu bildirim\n• IsCustomized → kullanıcı bu tür/kanal için kendi tercihini yazmış\nRecipientType — kural kimi hedefler:\n• All → tüm aktif kullanıcılar\n• User → RecipientId kullanıcı adıdır (AbpUsers.UserName)\n• Role → RecipientId rol adıdır; role sahip herkes alır\n• OrganizationUnit → RecipientId birim kodudur; birim ve tüm alt birimleri alır\n• Custom → RecipientId doğrudan adresin kendisidir (telefon, e-posta, chat id); kullanıcı araması yapılmaz, böylece sistem dışındaki birine de haber verilebilir\nKanal kurulumu — Custom dışındaki her kanalda adres kullanıcı kaydından çözülür:\n• Mail → AbpUsers.Email. Gönderim ayarları: Abp.Mailing.* ve App.Sender.Mail.*\n• Sms → AbpUsers.PhoneNumber. Ayarlar: App.Sender.Sms.PostaGuvercini.* (Url, Username, Password)\n• WhatsApp → AbpUsers.PhoneNumber, +90… biçimine normalize edilir. Ayarlar: App.Sender.WhatsApp.Url, PhoneNumberId, Token, TemplateName. Mesaj onaylı şablon üzerinden gider\n• Telegram → AbpUsers.TelegramChatId. Ayarlar: App.Sender.Telegram.Url, BotToken, ParseMode. Chat id nasıl alınır: kullanıcı bota bir kez /start yazar, ardından https://api.telegram.org/bot<BotToken>/getUpdates çıktısındaki result[].message.chat.id değeri okunur ve Identity › Kullanıcılar ekranındaki Telegram Chat Id alanına yazılır (kullanıcı kendi profilinde salt okunur görür). /start yazılmadan gönderim 'chat not found' hatası verir\n• Rocket → kullanıcının RocketUsername alanı. Ayarlar: App.Sender.Rocket.Url, UserId, Token\n• Desktop / UiActivity / UiToast → kullanıcının kendi kimliği; uygulama içinde iletilir (aktivite kaydı, toast, tarayıcı bildirimi)\nAkış: kural bir Notification satırı üretir, ardından NotificationWorker (Background Workers › Recurring Jobs, WorkerType = NotificationWorker, varsayılan cron */5) kuyruğu kanal kanal boşaltır.\nDikkat: kullanıcı tercihi (IsCustomized + RecipientType = User) bir kural değildir: yalnızca kendi sahibini o tür ve kanalda kapatır, IsFixed kuralda ise dikkate alınmaz. Alıcıları kurallar belirler; aynı kullanıcıyı birden çok kural hedeflerse kanal başına tek kayıt üretilir. Kanal adresi kullanıcıda boş olan kural sessizce atlanır — mesaj gitmediğinde önce Bildirimler ekranına bak: Identifier boşsa adres eksiktir, IsSent = false ise gönderim hata alıyordur."
},
{
"resourceName": "Platform",
"key": "App.Notifications.Notification.Description",
"en": "Notifications — the produced notification queue and history. Table: Sas_H_Notification · host scope · list screen.\nUsage: the first screen to look at when a notification does not arrive. Every row is one message produced for one recipient; the worker empties this queue.\nFields:\n• NotificationTypeId → which event produced it; the lookup comes from the Notification Types screen\n• NotificationChannel → the channel it will be delivered through\n• Identifier → the address actually used: e-mail, phone, chat id or user id. An empty value means the address of that channel is missing on the user record\n• Message → the message text\n• IsSent → has it been delivered to the channel. False means the worker has not run yet or the send keeps failing\n• IsRead, ReadTime → has the user read it and when; in-application channels (UiActivity, UiToast, Desktop) write these fields\nDiagnosis: no row at all → the rule is inactive or the type does not match. Row exists, IsSent = false → look at the run history in Background Workers › Jobs and at the application log. Row exists, IsSent = true but no message → the channel's own setting (token, template, sender address) is wrong.\nCaution: rows are not entered by hand — they are produced by the rule or by SendNotificationAsync. When the table grows, clean the old period with a scheduled SqlWorker.",
"tr": "Bildirimler — üretilmiş bildirim kuyruğu ve geçmişi. Tablo: Sas_H_Notification · host kapsamlı · liste ekranı.\nKullanım: bildirim gitmediğinde ilk bakılacak ekran. Her satır bir alıcı için üretilmiş bir mesajdır; worker bu kuyruğu boşaltır.\nAlanlar:\n• NotificationTypeId → hangi olayın ürettiği; lookup Bildirim Türleri ekranından gelir\n• NotificationChannel → iletileceği kanal\n• Identifier → gerçekte kullanılan adres: e-posta, telefon, chat id veya kullanıcı kimliği. Boş olması, kullanıcı kaydında o kanalın adresinin bulunmadığı anlamına gelir\n• Message → mesaj metni\n• IsSent → kanala iletildi mi. False ise worker henüz çalışmamıştır ya da gönderim sürekli hata alıyordur\n• IsRead, ReadTime → kullanıcı okudu mu, ne zaman; uygulama içi kanallar (UiActivity, UiToast, Desktop) bu alanları yazar\nTeşhis: hiç satır yoksa → kural pasiftir ya da tür eşleşmiyordur. Satır var, IsSent = false ise → Background Workers › Jobs ekranındaki çalışma geçmişine ve uygulama loguna bak. Satır var, IsSent = true ama mesaj gelmediyse → kanalın kendi ayarı (token, şablon, gönderen adresi) hatalıdır.\nDikkat: satırlar elle girilmez — kural ya da SendNotificationAsync üretir. Tablo büyüdüğünde eski dönemi zamanlanmış bir SqlWorker ile temizle."
"en": "Notifications — the produced notification queue and history. Table: Adm_T_Notification · tenant scope · list screen.\nUsage: the first screen to look at when a notification does not arrive. Every row is one message produced for one recipient; the worker empties this queue.\nFields:\n• NotificationTypeId → which event produced it; the lookup comes from the Notification Types screen\n• NotificationChannel → the channel it will be delivered through\n• Identifier → the address actually used: e-mail, phone, chat id or user id. An empty value means the address of that channel is missing on the user record\n• Message → the message text\n• IsSent → has it been delivered to the channel. False means the worker has not run yet or the send keeps failing\n• IsRead, ReadTime → has the user read it and when; in-application channels (UiActivity, UiToast, Desktop) write these fields\nDiagnosis: no row at all → the rule is inactive or the type does not match. Row exists, IsSent = false → look at the run history in Background Workers › Jobs and at the application log. Row exists, IsSent = true but no message → the channel's own setting (token, template, sender address) is wrong.\nManual send: the \"add\" form of this screen is the manual send. Pick the recipients and the channels (both are multi-select), optionally a template, then the subject and the body; the endpoint writes one row per recipient x channel with the \"Manuel\" notification type. Rows cannot be updated — a row is the log of a send. When the table grows, clean the old period with a scheduled SqlWorker.",
"tr": "Bildirimler — üretilmiş bildirim kuyruğu ve geçmişi. Tablo: Adm_T_Notification · kiracı kapsamlı · liste ekranı.\nKullanım: bildirim gitmediğinde ilk bakılacak ekran. Her satır bir alıcı için üretilmiş bir mesajdır; worker bu kuyruğu boşaltır.\nAlanlar:\n• NotificationTypeId → hangi olayın ürettiği; lookup Bildirim Türleri ekranından gelir\n• NotificationChannel → iletileceği kanal\n• Identifier → gerçekte kullanılan adres: e-posta, telefon, chat id veya kullanıcı kimliği. Boş olması, kullanıcı kaydında o kanalın adresinin bulunmadığı anlamına gelir\n• Message → mesaj metni\n• IsSent → kanala iletildi mi. False ise worker henüz çalışmamıştır ya da gönderim sürekli hata alıyordur\n• IsRead, ReadTime → kullanıcı okudu mu, ne zaman; uygulama içi kanallar (UiActivity, UiToast, Desktop) bu alanları yazar\nTeşhis: hiç satır yoksa → kural pasiftir ya da tür eşleşmiyordur. Satır var, IsSent = false ise → Background Workers › Jobs ekranındaki çalışma geçmişine ve uygulama loguna bak. Satır var, IsSent = true ama mesaj gelmediyse → kanalın kendi ayarı (token, şablon, gönderen adresi) hatalıdır.\nElle gönderim: bu ekranın \"ekle\" formu elle gönderimdir. Alıcıları ve kanalları seç (ikisi de çoklu), istersen bir şablon, sonra konu ve gövdeyi yaz; uç her alıcı x kanal açılımı için \"Manuel\" bildirim tipiyle bir satır yazar. Satırlar güncellenemez — satır bir gönderimin günlüğüdür. Tablo büyüdüğünde eski dönemi zamanlanmış bir SqlWorker ile temizle."
},
{
"resourceName": "Platform",
@ -28772,40 +28784,28 @@
},
{
"resourceName": "Platform",
"key": "App.ManualNotification.RecipientAndChannelRequired",
"key": "App.Notifications.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",
"key": "App.Notifications.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",
"key": "App.Notifications.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",
"key": "App.Notifications.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",
@ -28824,48 +28824,12 @@
"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",
@ -29099,6 +29063,36 @@
"key": "App.ImageUpload.TakePhoto",
"en": "Take photo with camera",
"tr": "Kamerayla fotoğraf çek"
},
{
"resourceName": "Platform",
"key": "App.Notifications.NoRecipientResolved",
"en": "No notification could be produced: none of the selected recipients has an address on the selected channels.",
"tr": "Hiç bildirim üretilemedi: seçilen alıcıların hiçbirinde seçilen kanalların adresi yok."
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.UserId",
"en": "Recipient",
"tr": "Alıcı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Scope",
"en": "Scope",
"tr": "Kapsam"
},
{
"resourceName": "Platform",
"key": "App.StaticLookup.Host",
"en": "Host",
"tr": "Host"
},
{
"resourceName": "Platform",
"key": "App.StaticLookup.HostAndTenant",
"en": "Host and Tenants",
"tr": "Host ve Kiracılar"
}
]
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
@ -7069,8 +7069,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
}
#endregion
#region Manual Notification
listFormName = AppCodes.ManualNotification;
#region Notification Type
listFormName = AppCodes.Notifications.NotificationTypes;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -7092,7 +7092,435 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
IsOrganizationUnit = false,
Description = $"{listFormName}.Description",
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationManual)),
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationType)),
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.NotificationType)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 420, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{
new() {
Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group" },
}),
});
#region Notification Type 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),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Name",
EditGroupOrderNo = 1,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.Name",
Width = 0,
ListOrderNo = 2,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Description",
EditGroupOrderNo = 1,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxTextArea,
EditorOptions = EditorOptions.Multiline(200),
ColSpan = 1,
CaptionName = "App.Listform.ListformField.Description",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
}
]);
#endregion
}
#endregion
#region Notification Rule
listFormName = AppCodes.Notifications.NotificationRules;
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.NotificationRule)),
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.NotificationRule)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 550, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group" },
new() { Order=2, ColCount=1, ColSpan=1, ItemType="group" },
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsFixed", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsCustomized", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
new CommandColumnDto() {
Hint = "Create",
Text = "Create",
AuthName = listFormName,
DialogName = "CreateNotification",
DialogParameters = JsonSerializer.Serialize(new { name = "id", id = "@Id" }),
IsVisible = true,
},
}),
}
);
#region Notification Rule Fields
await _listFormFieldRepository.InsertManyAsync([
new ListFormField
{
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.Guid,
FieldName = "NotificationTypeId",
EditGroupOrderNo = 1,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.NotificationTypeId",
Width = 0,
ListOrderNo = 2,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.NotificationType), "Id", "Name"),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "RecipientType",
EditGroupOrderNo = 1,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.RecipientType",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="All", Name="App.StaticLookup.All" },
new () { Key="User", Name="App.Platform.User" },
new () { Key="Role", Name="App.StaticLookup.Role" },
new () { Key="OrganizationUnit", Name="App.Listform.ListformField.IsOrganizationUnit" },
new () { Key="Custom", Name="App.StaticLookup.Custom" },
}),
}),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "RecipientId",
EditGroupOrderNo = 1,
EditOrderNo = 4,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.RecipientId",
Width = 0,
ListOrderNo = 4,
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 = "Channel",
EditGroupOrderNo = 1,
EditOrderNo = 3,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.Channel",
Width = 0,
ListOrderNo = 5,
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.Boolean,
FieldName = "IsActive",
EditGroupOrderNo = 1,
EditOrderNo = 5,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsActive",
Width = 0,
ListOrderNo = 6,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsFixed",
EditGroupOrderNo = 1,
EditOrderNo = 6,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsFixed",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsCustomized",
EditGroupOrderNo = 1,
EditOrderNo = 7,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsCustomized",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailType",
CaptionName = "Options:MailType",
EditGroupOrderNo = 2,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
Visible = false,
IsActive = false,
ListOrderNo = 9,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailSubject",
CaptionName = "Options:MailSubject",
EditGroupOrderNo = 2,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
Visible = false,
IsActive = false,
ListOrderNo = 10,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailTemplate",
CaptionName = "Options:MailTemplate",
EditGroupOrderNo = 2,
EditOrderNo = 3,
EditorType2 = EditorTypes.dxHtmlEditor,
ColSpan = 2,
EditorOptions = EditorOptions.Html(),
Visible = false,
IsActive = false,
ListOrderNo = 11,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:TableName",
CaptionName = "Options:TableName",
EditGroupOrderNo = 2,
EditOrderNo = 4,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 2,
Visible = false,
IsActive = false,
ListOrderNo = 12,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
]);
#endregion
}
#endregion
// Bildirim ekrani hem gunluk hem gonderim ekranidir: liste kuyruktaki her satiri
// (kural motorunun urettikleri dahil) gosterir, ekleme formu ise elle gonderimdir.
// Ayri bir manuel gonderim kaydi yoktur - gonderimin kaydi bildirimin kendisidir.
#region Notification
listFormName = AppCodes.Notifications.Notification;
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.Notification)),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = DefaultFilterJson,
@ -7104,22 +7532,23 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
SelectionJson = DefaultSelectionSingleJson,
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.NotificationManual)),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Notification)),
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",
// devredilir: uc secilen alici x kanal acilimini NotificationManager ile
// kuyruga yazar. Guncelleme kapali, cunku kayit gonderimin gunlugudur.
InsertServiceAddress = "list-form-dynamic-api/notification-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
#region Notification Fields
await _listFormFieldRepository.InsertManyAsync([
new() {
ListFormCode = listForm.ListFormCode,
@ -7154,6 +7583,22 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "NotificationTypeId",
CaptionName = "App.Listform.ListformField.NotificationTypeId",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.NotificationType), "Id", "Name"),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -7166,7 +7611,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
EditorOptions = EditorOptions.ShowClearButton().Flag("searchEnabled", true),
ColSpan = 2,
Width = 0,
ListOrderNo = 3,
ListOrderNo = 4,
Visible = true,
IsActive = true,
AllowSearch = true,
@ -7187,12 +7632,14 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
// Kanal ve alici listede tek degerlidir (her satir bir alici x kanal), formda
// coklu secilir: uc secimi acar ve her acilim icin bir satir yazar.
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Channels",
CaptionName = "App.Listform.ListformField.Channels",
FieldName = "NotificationChannel",
CaptionName = "App.Listform.ListformField.NotificationChannel",
EditGroupOrderNo = 1,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxTagBox,
@ -7203,7 +7650,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
.Text("applyValueMode", "useButtons"),
ColSpan = 2,
Width = 0,
ListOrderNo = 4,
ListOrderNo = 5,
Visible = true,
IsActive = true,
AllowSearch = true,
@ -7216,9 +7663,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "UserIds",
CaptionName = "App.Listform.ListformField.UserIds",
SourceDbType = DbType.Guid,
FieldName = "UserId",
CaptionName = "App.Listform.ListformField.UserId",
EditGroupOrderNo = 1,
EditOrderNo = 3,
EditorType2 = EditorTypes.dxTagBox,
@ -7230,7 +7677,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
.Number("maxDisplayedTags", 6),
ColSpan = 2,
Width = 0,
ListOrderNo = 5,
ListOrderNo = 6,
Visible = true,
IsActive = true,
AllowSearch = true,
@ -7245,6 +7692,27 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Identifier",
CaptionName = "App.Listform.ListformField.Identifier",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.NotificationIdentityValues
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -7257,7 +7725,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
EditorOptions = EditorOptions.ShowClearButton().MaxLength(256),
ColSpan = 2,
Width = 0,
ListOrderNo = 6,
ListOrderNo = 8,
Visible = true,
IsActive = true,
AllowSearch = true,
@ -7277,39 +7745,6 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
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,
@ -7321,15 +7756,13 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "SkippedCount",
CaptionName = "App.Listform.ListformField.SkippedCount",
EditorType2 = EditorTypes.dxNumberBox,
SourceDbType = DbType.Boolean,
FieldName = "IsSent",
CaptionName = "App.Listform.ListformField.IsSent",
Width = 0,
ListOrderNo = 10,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
@ -7337,15 +7770,27 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ResultNote",
CaptionName = "App.Listform.ListformField.ResultNote",
EditorType2 = EditorTypes.dxTextBox,
SourceDbType = DbType.Boolean,
FieldName = "IsRead",
CaptionName = "App.Listform.ListformField.IsRead",
Width = 0,
ListOrderNo = 11,
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 = "ReadTime",
CaptionName = "App.Listform.ListformField.ReadTime",
Width = 0,
ListOrderNo = 12,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson

View file

@ -5175,644 +5175,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
}
#endregion
#region Notification Type
listFormName = AppCodes.Notifications.NotificationTypes;
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 = false,
IsBranch = false,
IsOrganizationUnit = false,
Description = $"{listFormName}.Description",
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationType)),
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.NotificationType)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 420, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{
new() {
Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group" },
}),
});
#region Notification Type 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),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Name",
EditGroupOrderNo = 1,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.Name",
Width = 0,
ListOrderNo = 2,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Description",
EditGroupOrderNo = 1,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxTextArea,
EditorOptions = EditorOptions.Multiline(200),
ColSpan = 1,
CaptionName = "App.Listform.ListformField.Description",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
}
]);
#endregion
}
#endregion
#region Notification Rule
listFormName = AppCodes.Notifications.NotificationRules;
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 = false,
IsBranch = false,
IsOrganizationUnit = false,
Description = $"{listFormName}.Description",
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.NotificationRule)),
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 = $"UPDATE \"{FullNameTable(TableNameEnum.NotificationRule)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 550, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group" },
new() { Order=2, ColCount=1, ColSpan=1, ItemType="group" },
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsFixed", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsCustomized", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
new CommandColumnDto() {
Hint = "Create",
Text = "Create",
AuthName = listFormName,
DialogName = "CreateNotification",
DialogParameters = JsonSerializer.Serialize(new { name = "id", id = "@Id" }),
IsVisible = true,
},
}),
}
);
#region Notification Rule Fields
await _listFormFieldRepository.InsertManyAsync([
new ListFormField
{
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.Guid,
FieldName = "NotificationTypeId",
EditGroupOrderNo = 1,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.NotificationTypeId",
Width = 0,
ListOrderNo = 2,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.NotificationType), "Id", "Name"),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "RecipientType",
EditGroupOrderNo = 1,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.RecipientType",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="All", Name="App.StaticLookup.All" },
new () { Key="User", Name="App.Platform.User" },
new () { Key="Role", Name="App.StaticLookup.Role" },
new () { Key="OrganizationUnit", Name="App.Listform.ListformField.IsOrganizationUnit" },
new () { Key="Custom", Name="App.StaticLookup.Custom" },
}),
}),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "RecipientId",
EditGroupOrderNo = 1,
EditOrderNo = 4,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.RecipientId",
Width = 0,
ListOrderNo = 4,
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 = "Channel",
EditGroupOrderNo = 1,
EditOrderNo = 3,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.Channel",
Width = 0,
ListOrderNo = 5,
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.Boolean,
FieldName = "IsActive",
EditGroupOrderNo = 1,
EditOrderNo = 5,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsActive",
Width = 0,
ListOrderNo = 6,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsFixed",
EditGroupOrderNo = 1,
EditOrderNo = 6,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsFixed",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsCustomized",
EditGroupOrderNo = 1,
EditOrderNo = 7,
EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsCustomized",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailType",
CaptionName = "Options:MailType",
EditGroupOrderNo = 2,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
Visible = false,
IsActive = false,
ListOrderNo = 9,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailSubject",
CaptionName = "Options:MailSubject",
EditGroupOrderNo = 2,
EditOrderNo = 2,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
Visible = false,
IsActive = false,
ListOrderNo = 10,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:MailTemplate",
CaptionName = "Options:MailTemplate",
EditGroupOrderNo = 2,
EditOrderNo = 3,
EditorType2 = EditorTypes.dxHtmlEditor,
ColSpan = 2,
EditorOptions = EditorOptions.Html(),
Visible = false,
IsActive = false,
ListOrderNo = 11,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Options:TableName",
CaptionName = "Options:TableName",
EditGroupOrderNo = 2,
EditOrderNo = 4,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 2,
Visible = false,
IsActive = false,
ListOrderNo = 12,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
]);
#endregion
}
#endregion
#region Notification
listFormName = AppCodes.Notifications.Notification;
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 = false,
IsBranch = false,
IsOrganizationUnit = false,
Description = $"{listFormName}.Description",
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Notification)),
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 = $"UPDATE \"{FullNameTable(TableNameEnum.Notification)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 500, false, false, false, false, false, false),
}
);
#region Notification Fields
await _listFormFieldRepository.InsertManyAsync([
new ListFormField
{
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 = "NotificationChannel",
CaptionName = "App.Listform.ListformField.NotificationChannel",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = NotificationChannelLookupJson(),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "NotificationTypeId",
EditGroupOrderNo = 1,
EditOrderNo = 1,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.NotificationTypeId",
Width = 0,
ListOrderNo = 4,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.NotificationType), "Id", "Name"),
// LookupJson = JsonSerializer.Serialize(new LookupDto {
// DataSourceType = UiLookupDataSourceTypeEnum.WebService,
// DisplayExpr = "Name",
// ValueExpr = "Key",
// LookupQuery = "GET;notification-rule/notification-types;{};a;a;null",
// }),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Identifier",
CaptionName = "App.Listform.ListformField.Identifier",
Width = 0,
ListOrderNo = 5,
Visible = true,
IsActive = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.NotificationIdentityValues
}),
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",
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,
SourceDbType = DbType.String,
FieldName = "Message",
CaptionName = "App.Listform.ListformField.Message",
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.Boolean,
FieldName = "IsSent",
CaptionName = "App.Listform.ListformField.IsSent",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsRead",
CaptionName = "App.Listform.ListformField.IsRead",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "ReadTime",
CaptionName = "App.Listform.ListformField.ReadTime",
Width = 0,
ListOrderNo = 9,
Visible = true,
IsActive = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
]);
#endregion
}
#endregion
#region Background Worker Recurring Jobs
listFormName = AppCodes.BackgroundWorkers.RecurringJobs;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
@ -5996,6 +5358,39 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "Scope",
EditGroupOrderNo = 1,
EditOrderNo = 4,
EditorType2 = EditorTypes.dxSelectBox,
ColSpan = 1,
EditorOptions = EditorOptions.ShowClearButton(),
CaptionName = "App.Listform.ListformField.Scope",
Width = 0,
ListOrderNo = 5,
Visible = true,
IsActive = true,
// Isin govdesinin hangi kiraci baglaminda calisacagi: host bir kez, kiraci her
// aktif kiraci icin, ikisi once host sonra kiracilar (WorkerScopeEnum).
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new() { Key=1, Name="App.StaticLookup.Host" },
new() { Key=2, Name="App.StaticLookup.Tenant" },
new() { Key=3, Name="App.StaticLookup.HostAndTenant" },
}),
}),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -6007,7 +5402,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ColSpan = 1,
CaptionName = "App.Listform.ListformField.BeforeSp",
Width = 0,
ListOrderNo = 5,
ListOrderNo = 6,
Visible = true,
IsActive = true,
@ -6027,7 +5422,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ColSpan = 1,
CaptionName = "App.Listform.ListformField.AfterSp",
Width = 0,
ListOrderNo = 6,
ListOrderNo = 7,
Visible = true,
IsActive = true,
@ -6043,7 +5438,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Options",
CaptionName = "App.Listform.ListformField.Options",
Width = 0,
ListOrderNo = 7,
ListOrderNo = 8,
Visible = true,
IsActive = true,
@ -6063,7 +5458,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsActive",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 9,
Visible = true,
IsActive = true,
@ -6150,6 +5545,22 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Tenant",
CaptionName = "App.Listform.ListformField.Tenant",
Width = 0,
ListOrderNo = 3,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -6157,7 +5568,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ExecutionDate",
CaptionName = "App.Listform.ListformField.ExecutionDate",
Width = 0,
ListOrderNo = 3,
ListOrderNo = 4,
Visible = true,
IsActive = true,
@ -6174,7 +5585,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Duration",
CaptionName = "App.Listform.ListformField.Duration",
Width = 0,
ListOrderNo = 4,
ListOrderNo = 5,
Visible = true,
IsActive = true,
@ -6190,7 +5601,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Status",
CaptionName = "App.Listform.ListformField.Status",
Width = 0,
ListOrderNo = 5,
ListOrderNo = 6,
Visible = true,
IsActive = true,
@ -6206,7 +5617,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "FailureReason",
CaptionName = "App.Listform.ListformField.FailureReason",
Width = 0,
ListOrderNo = 6,
ListOrderNo = 7,
Visible = true,
IsActive = true,
@ -6221,7 +5632,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ExceptionType",
CaptionName = "App.Listform.ListformField.ExceptionType",
Width = 0,
ListOrderNo = 7,
ListOrderNo = 8,
Visible = true,
IsActive = true,
@ -6237,7 +5648,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ExceptionDetails",
CaptionName = "App.Listform.ListformField.ExceptionDetails",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 9,
Visible = true,
IsActive = true,

View file

@ -125,7 +125,9 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
else
_logger.LogInformation("Executing SQL batch from {FileName}", fileName);
await dbContext.Database.ExecuteSqlRawAsync(sql);
// ExecuteSqlRaw metni String.Format'tan gecirir: seed scriptindeki her susu
// parantez (yorum, regex, JSON varsayilani) kacisli yazilmazsa yer tutucu sanilir.
await dbContext.Database.ExecuteSqlRawAsync(EscapeBraces(sql));
}
catch (Exception ex)
{
@ -228,6 +230,12 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
return ("UNKNOWN", null, null);
}
/// <summary>Suslu parantezi literal hale getirir; bkz. cagri yerindeki not.</summary>
private static string EscapeBraces(string sql)
{
return sql.Replace("{", "{{").Replace("}", "}}");
}
private static string RemoveComments(string sql)
{
var withoutBlockComments = Regex.Replace(sql, @"/\*.*?\*/", " ", RegexOptions.Singleline);

View file

@ -10,7 +10,6 @@ public enum TableNameEnum
NotificationRule,
NotificationType,
NotificationTemplate,
NotificationManual,
BackgroundWorker,
BackgroundWorker_MailQueue,
BackgroundWorker_MailQueueEvents,

View file

@ -0,0 +1,18 @@
namespace Sozsoft.Platform.Enums;
/// <summary>
/// Zamanlanmis bir isin hangi veri kapsaminda calisacagi. Is kaydinin kendisi her zaman host
/// tablosundadir (<c>Sas_H_BackgroundWorker</c>); kapsam, isin govdesinin hangi kiraci
/// baglaminda kac kez calistirilacagini belirler.
/// </summary>
public enum WorkerScopeEnum
{
/// <summary>Yalnizca host baglaminda bir kez calisir; kiraci verisine dokunmaz.</summary>
Host = 1,
/// <summary>Her aktif kiraci icin bir kez, o kiracinin baglaminda calisir.</summary>
Tenant = 2,
/// <summary>Once host, sonra her aktif kiraci icin calisir.</summary>
All = 3,
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Enums;
@ -457,18 +457,15 @@ public static class PlatformConsts
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";
/// <summary>
/// Bildirim ekraninin islem yetkileri. Kayit olusturmak gonderim demektir; bu yuzden
/// elle gonderim ayri bir yetki degil bu ekranin <c>Create</c> yetkisidir.
/// </summary>
public static class NotificationCommands
{
public const string Create = Notification + ".Create";
}
}
public static class BackgroundWorkers

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
namespace Sozsoft.Platform.Enums;
@ -41,9 +41,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.ListFormImport), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.ListFormImportLog), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.ListFormWorkflow), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Notification), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.NotificationRule), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.NotificationType), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.BackgroundWorker), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.BackgroundWorker_JobFlow), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.BackgroundWorker_MailQueue), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
@ -87,7 +84,9 @@ public static class TableNameResolver
{ 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) },
{ nameof(TableNameEnum.NotificationType), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.NotificationRule), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.Notification), (TablePrefix.TenantByName, MenuPrefix.Administration) },
// 🔹 INTRANET TABLOLARI
{ nameof(TableNameEnum.IntranetWidget), (TablePrefix.TenantByName, MenuPrefix.Sas) },

View file

@ -26,7 +26,7 @@ namespace Sozsoft.Platform.BackgroundWorkers;
public interface IPlatformBackgroundWorker : ITransientDependency
{
Task StartAsync(Guid WorkerId, CancellationToken cancellationToken = default);
Task StartAsync(Guid WorkerId, Guid? TenantId, CancellationToken cancellationToken = default);
}
public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgroundWorker
@ -55,17 +55,20 @@ public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgrou
Repository = repository;
}
public async Task StartAsync(Guid WorkerId, CancellationToken cancellationToken = default)
/// <summary>
/// Tek bir kapsamin (host ya da bir kiraci) calismasi. Zamanlama tarafinda her kapsam icin
/// ayri bir Hangfire isi kurulur; boylece is kaydi hangi kiracida ne kadar surdugunu tasir.
/// </summary>
public async Task StartAsync(Guid WorkerId, Guid? TenantId, CancellationToken cancellationToken = default)
{
using var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin(requiresNew: true, isTransactional: false);
var Worker = await Repository.FirstOrDefaultAsync(a => a.Id == WorkerId, cancellationToken: cancellationToken);
var LogPrefix = $"{Clock.Now:s}_{Worker.Name}: {{0}}";
var DistributedLockName = Worker.Name;
var backupPath = "";
//using (var scope = ServiceScopeFactory.CreateScope())
// Kilit kapsam basinadir: bir kiracinin calismasi digerlerini bekletmez.
var DistributedLockName = TenantId is null ? Worker.Name : $"{Worker.Name}#{TenantId}";
await using var handle = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: cancellationToken);
@ -77,18 +80,30 @@ public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgrou
return;
}
// Backup Worker için yedeklerin saklanacağı klasörün oluşturulması
if (Worker.WorkerType == WorkerTypeEnum.BackupWorker)
{
var backupPath = Configuration["App:BackupPath"];
if (!Directory.Exists(backupPath))
{
Directory.CreateDirectory(backupPath);
}
}
using (CurrentTenant.Change(TenantId))
{
await RunScopeAsync(Worker, LogPrefix, cancellationToken);
}
Logger.LogInformation(LogPrefix, $"Lock is released for {DistributedLockName}");
return;
}
/// <summary>Isin govdesi: oncesi/sonrasi saklı yordamlar ve tipine gore calisan worker.</summary>
private async Task RunScopeAsync(Entities.BackgroundWorker Worker, string LogPrefix, CancellationToken cancellationToken)
{
try
{
// Backup Worker için yedeklerin saklanacağı klasörün oluşturulması
if (Worker.WorkerType == WorkerTypeEnum.BackupWorker)
{
backupPath = Configuration["App:BackupPath"];
if (!Directory.Exists(backupPath))
{
Directory.CreateDirectory(backupPath);
}
}
// Call BeforeSp
if (!Worker.BeforeSp.IsNullOrWhiteSpace())
{
@ -136,10 +151,5 @@ public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgrou
Logger.LogError(LogPrefix, ex.ToString());
Logger.LogException(ex);
}
Logger.LogInformation(LogPrefix, $"Lock is released for {DistributedLockName}");
return;
}
}

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Notifications.Entities;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bildirim tipleri ve varsayilan kurallari kiraci kapsamindadir: her kiraci kendi turunu ve
/// kuralini duzenler. Host kapsamindaki seed dosyalari varsayilan kumeyi tanimlar, bu contributor
/// da yeni kiraciya o kumenin bir kopyasini uretir.
/// <para>
/// Kopya satirlar yeni kimliklerle yazilir; dosyadaki kimlik host kaydinindir ve tum kiracilarda
/// tekrar edilemez. Eslesme ad uzerinden yapilir, bu yuzden ayni kiraci icin ikinci kez
/// calistirildiginda hicbir sey eklenmez.
/// </para>
/// </summary>
public class NotificationTenantDefaultsSeedContributor(
IRepository<NotificationType, Guid> notificationTypeRepository,
IRepository<NotificationRule, Guid> notificationRuleRepository,
IConfiguration configuration,
ILogger<NotificationTenantDefaultsSeedContributor> logger) : IDataSeedContributor, ITransientDependency
{
private const string TypesFile = "App.Notifications.NotificationTypes";
private const string RulesFile = "App.Notifications.NotificationRules";
public async Task SeedAsync(DataSeedContext context)
{
// Host kapsami kendi dosyalarini ListFormSeedDataApplier ile uygular; burada yalnizca
// kiraci kopyasi uretilir.
if (context.TenantId == null)
{
return;
}
var types = ReadRows(TypesFile);
if (types.Count == 0)
{
return;
}
var typeIdByFileId = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
var existingTypes = await notificationTypeRepository.GetListAsync();
foreach (var row in types)
{
var name = GetString(row, "Name");
if (name.IsNullOrWhiteSpace())
{
continue;
}
var existing = existingTypes.Find(type => type.Name == name);
if (existing == null)
{
existing = await notificationTypeRepository.InsertAsync(
new NotificationType()
{
TenantId = context.TenantId,
Name = name!,
Description = GetString(row, "Description"),
},
autoSave: true);
}
var fileId = GetString(row, "Id");
if (!fileId.IsNullOrWhiteSpace())
{
typeIdByFileId[fileId!] = existing.Id;
}
}
var existingRules = await notificationRuleRepository.GetListAsync();
foreach (var row in ReadRows(RulesFile))
{
var fileTypeId = GetString(row, "NotificationTypeId");
if (fileTypeId == null || !typeIdByFileId.TryGetValue(fileTypeId, out var typeId))
{
continue;
}
var recipientType = GetString(row, "RecipientType");
var recipientId = GetString(row, "RecipientId");
var channel = GetString(row, "Channel");
var duplicate = existingRules.Exists(rule =>
rule.NotificationTypeId == typeId
&& rule.RecipientType == recipientType
&& rule.RecipientId == recipientId
&& rule.Channel == channel);
if (duplicate)
{
continue;
}
await notificationRuleRepository.InsertAsync(
new NotificationRule()
{
TenantId = context.TenantId,
NotificationTypeId = typeId,
RecipientType = recipientType!,
RecipientId = recipientId,
Channel = channel!,
IsActive = GetBool(row, "IsActive"),
IsFixed = GetBool(row, "IsFixed"),
IsCustomized = GetBool(row, "IsCustomized"),
},
autoSave: true);
}
}
/// <summary>
/// Varsayilan kumeyi host kapsamindaki veri seed dosyasindan okur; ikinci bir varsayilan
/// listesi kodda tutulmaz. Dosya yoksa kiraci bos baslar, bu bir hata degildir.
/// </summary>
private List<JsonElement> ReadRows(string listFormCode)
{
var path = SeedPathResolver.GetScopePath(
configuration,
null,
SeedPathResolver.GetDataFilePath(listFormCode).Split('/'));
if (!File.Exists(path))
{
logger.LogInformation("Notification defaults file not found, skipping: {Path}", path);
return [];
}
using var document = JsonDocument.Parse(File.ReadAllText(path));
return document.RootElement.TryGetProperty("Rows", out var rows) && rows.ValueKind == JsonValueKind.Array
? [.. rows.EnumerateArray().Select(row => row.Clone())]
: [];
}
private static string? GetString(JsonElement row, string propertyName)
{
return row.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
}
private static bool GetBool(JsonElement row, string propertyName)
{
return row.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.True;
}
}

View file

@ -499,7 +499,6 @@ 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

@ -1,4 +1,4 @@
using System;
using System;
using Sozsoft.Platform.Enums;
using Volo.Abp.Domain.Entities.Auditing;
@ -9,6 +9,12 @@ public class BackgroundWorker : FullAuditedEntity<Guid>
public string Name { get; set; }
public string Cron { get; set; }
public WorkerTypeEnum WorkerType { get; set; }
/// <summary>
/// Isin hangi veri kapsaminda calisacagi. Kayit host tablosunda durur; kapsam govdenin
/// kac kiraci baglaminda calisacagini belirler (bkz. <see cref="WorkerScopeEnum"/>).
/// </summary>
public WorkerScopeEnum Scope { get; set; } = WorkerScopeEnum.Host;
public bool IsActive { get; set; }
public string DataSourceCode { get; set; }
public string BeforeSp { get; set; }

View file

@ -1,36 +0,0 @@
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

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
@ -102,7 +102,6 @@ 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
@ -570,6 +569,12 @@ public class PlatformDbContext :
b.Property(a => a.Name).IsRequired().HasMaxLength(128);
b.Property(a => a.Cron).IsRequired().HasMaxLength(64);
b.Property(a => a.WorkerType).IsRequired();
// Sentinel acikca yazilir: 0 gecerli bir WorkerScopeEnum degeri degildir, yani
// kapsami yazilmadan eklenen satir veritabani varsayilanini (Host) alir. Sentinel
// verilmezse EF ayni davranisi uygular ama her model kurulumunda uyari yazar.
b.Property(a => a.Scope).IsRequired()
.HasDefaultValue(WorkerScopeEnum.Host)
.HasSentinel(default(WorkerScopeEnum));
b.Property(a => a.IsActive).HasDefaultValue(true);
b.Property(a => a.DataSourceCode).HasMaxLength(64);
b.Property(a => a.BeforeSp).HasMaxLength(128);
@ -1615,22 +1620,6 @@ public class PlatformDbContext :
.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("20260921060400_Initial")]
[Migration("20260922094111_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -571,6 +571,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("NotificationRuleId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("NotificationTemplateId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("NotificationTypeId")
.HasColumnType("uniqueidentifier");
@ -581,6 +584,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
@ -590,7 +597,9 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("NotificationTypeId");
b.ToTable("Sas_H_Notification", (string)null);
b.HasIndex("TenantId", "IsSent", "NotificationChannel");
b.ToTable("Adm_T_Notification", (string)null);
});
modelBuilder.Entity("Sozsoft.Notifications.Entities.NotificationRule", b =>
@ -655,11 +664,17 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("NotificationTypeId");
b.ToTable("Sas_H_NotificationRule", (string)null);
b.HasIndex("TenantId", "NotificationTypeId");
b.ToTable("Adm_T_NotificationRule", (string)null);
});
modelBuilder.Entity("Sozsoft.Notifications.Entities.NotificationType", b =>
@ -707,9 +722,15 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_H_NotificationType", (string)null);
b.HasIndex("TenantId", "Name");
b.ToTable("Adm_T_NotificationType", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b =>
@ -1094,6 +1115,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Options")
.HasColumnType("nvarchar(max)");
b.Property<int>("Scope")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<int>("WorkerType")
.HasColumnType("int");
@ -4307,89 +4333,6 @@ 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")

View file

@ -677,34 +677,6 @@ 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
@ -730,6 +702,27 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Adm_T_NotificationTemplate", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_NotificationType",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, 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_NotificationType", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_ReportCategory",
columns: table => new
@ -1001,6 +994,7 @@ namespace Sozsoft.Platform.Migrations
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Cron = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
WorkerType = table.Column<int>(type: "int", nullable: false),
Scope = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
DataSourceCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
BeforeSp = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
@ -1583,26 +1577,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_H_Module", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Sas_H_NotificationType",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, 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_Sas_H_NotificationType", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Sas_H_Order",
columns: table => new
@ -2319,6 +2293,38 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_NotificationRule",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
NotificationTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RecipientType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
RecipientId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Channel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsFixed = table.Column<bool>(type: "bit", nullable: false),
IsCustomized = table.Column<bool>(type: "bit", nullable: false),
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_NotificationRule", x => x.Id);
table.ForeignKey(
name: "FK_Adm_T_NotificationRule_Adm_T_NotificationType_NotificationTypeId",
column: x => x.NotificationTypeId,
principalTable: "Adm_T_NotificationType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Adm_T_ReportTemplate",
columns: table => new
@ -2969,37 +2975,6 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Sas_H_NotificationRule",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NotificationTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RecipientType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
RecipientId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Channel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsFixed = table.Column<bool>(type: "bit", nullable: false),
IsCustomized = table.Column<bool>(type: "bit", nullable: false),
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_Sas_H_NotificationRule", x => x.Id);
table.ForeignKey(
name: "FK_Sas_H_NotificationRule_Sas_H_NotificationType_NotificationTypeId",
column: x => x.NotificationTypeId,
principalTable: "Sas_H_NotificationType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Sas_H_OrderItem",
columns: table => new
@ -3241,6 +3216,48 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_Notification",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
NotificationRuleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NotificationChannel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
NotificationTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NotificationTemplateId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
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),
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_Notification", x => x.Id);
table.ForeignKey(
name: "FK_Adm_T_Notification_Adm_T_NotificationRule_NotificationRuleId",
column: x => x.NotificationRuleId,
principalTable: "Adm_T_NotificationRule",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Adm_T_Notification_Adm_T_NotificationType_NotificationTypeId",
column: x => x.NotificationTypeId,
principalTable: "Adm_T_NotificationType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Adm_T_SocialPollOption",
columns: table => new
@ -3454,46 +3471,6 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sas_H_Notification",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NotificationRuleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NotificationChannel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
NotificationTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
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),
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_Sas_H_Notification", x => x.Id);
table.ForeignKey(
name: "FK_Sas_H_Notification_Sas_H_NotificationRule_NotificationRuleId",
column: x => x.NotificationRuleId,
principalTable: "Sas_H_NotificationRule",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Sas_H_Notification_Sas_H_NotificationType_NotificationTypeId",
column: x => x.NotificationTypeId,
principalTable: "Sas_H_NotificationType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Sas_T_ForumPost",
columns: table => new
@ -3882,9 +3859,29 @@ namespace Sozsoft.Platform.Migrations
columns: new[] { "TenantId", "ConversationId", "SentAt" });
migrationBuilder.CreateIndex(
name: "IX_Adm_T_NotificationManual_TenantId_CreationTime",
table: "Adm_T_NotificationManual",
columns: new[] { "TenantId", "CreationTime" });
name: "IX_Adm_T_Notification_NotificationRuleId",
table: "Adm_T_Notification",
column: "NotificationRuleId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_Notification_NotificationTypeId",
table: "Adm_T_Notification",
column: "NotificationTypeId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_Notification_TenantId_IsSent_NotificationChannel",
table: "Adm_T_Notification",
columns: new[] { "TenantId", "IsSent", "NotificationChannel" });
migrationBuilder.CreateIndex(
name: "IX_Adm_T_NotificationRule_NotificationTypeId",
table: "Adm_T_NotificationRule",
column: "NotificationTypeId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_NotificationRule_TenantId_NotificationTypeId",
table: "Adm_T_NotificationRule",
columns: new[] { "TenantId", "NotificationTypeId" });
migrationBuilder.CreateIndex(
name: "IX_Adm_T_NotificationTemplate_TenantId_Code",
@ -3893,6 +3890,11 @@ namespace Sozsoft.Platform.Migrations
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_NotificationType_TenantId_Name",
table: "Adm_T_NotificationType",
columns: new[] { "TenantId", "Name" });
migrationBuilder.CreateIndex(
name: "IX_Adm_T_ReportCategory_TenantId_Name",
table: "Adm_T_ReportCategory",
@ -4274,21 +4276,6 @@ namespace Sozsoft.Platform.Migrations
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Sas_H_Notification_NotificationRuleId",
table: "Sas_H_Notification",
column: "NotificationRuleId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_Notification_NotificationTypeId",
table: "Sas_H_Notification",
column: "NotificationTypeId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_NotificationRule_NotificationTypeId",
table: "Sas_H_NotificationRule",
column: "NotificationTypeId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_OrderItem_OrderId",
table: "Sas_H_OrderItem",
@ -4511,7 +4498,7 @@ namespace Sozsoft.Platform.Migrations
name: "Adm_T_Note");
migrationBuilder.DropTable(
name: "Adm_T_NotificationManual");
name: "Adm_T_Notification");
migrationBuilder.DropTable(
name: "Adm_T_NotificationTemplate");
@ -4636,9 +4623,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Sas_H_Menu");
migrationBuilder.DropTable(
name: "Sas_H_Notification");
migrationBuilder.DropTable(
name: "Sas_H_OrderItem");
@ -4705,6 +4689,9 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Adm_T_MessengerConversation");
migrationBuilder.DropTable(
name: "Adm_T_NotificationRule");
migrationBuilder.DropTable(
name: "Adm_T_ReportCategory");
@ -4744,9 +4731,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Sas_H_Module");
migrationBuilder.DropTable(
name: "Sas_H_NotificationRule");
migrationBuilder.DropTable(
name: "Sas_H_Order");
@ -4768,6 +4752,9 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Adm_T_EventType");
migrationBuilder.DropTable(
name: "Adm_T_NotificationType");
migrationBuilder.DropTable(
name: "Adm_T_SocialPost");
@ -4786,9 +4773,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Sas_H_ListForm");
migrationBuilder.DropTable(
name: "Sas_H_NotificationType");
migrationBuilder.DropTable(
name: "Sas_T_ForumCategory");

View file

@ -568,6 +568,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("NotificationRuleId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("NotificationTemplateId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("NotificationTypeId")
.HasColumnType("uniqueidentifier");
@ -578,6 +581,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
@ -587,7 +594,9 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("NotificationTypeId");
b.ToTable("Sas_H_Notification", (string)null);
b.HasIndex("TenantId", "IsSent", "NotificationChannel");
b.ToTable("Adm_T_Notification", (string)null);
});
modelBuilder.Entity("Sozsoft.Notifications.Entities.NotificationRule", b =>
@ -652,11 +661,17 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("NotificationTypeId");
b.ToTable("Sas_H_NotificationRule", (string)null);
b.HasIndex("TenantId", "NotificationTypeId");
b.ToTable("Adm_T_NotificationRule", (string)null);
});
modelBuilder.Entity("Sozsoft.Notifications.Entities.NotificationType", b =>
@ -704,9 +719,15 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_H_NotificationType", (string)null);
b.HasIndex("TenantId", "Name");
b.ToTable("Adm_T_NotificationType", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b =>
@ -1091,6 +1112,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Options")
.HasColumnType("nvarchar(max)");
b.Property<int>("Scope")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<int>("WorkerType")
.HasColumnType("int");
@ -4304,89 +4330,6 @@ 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")

View file

@ -216,6 +216,15 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Develop`'a ek olarak `App.SqlQueryManager`
ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir
(`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir.
- **Bildirim kapsamı** → bildirim tipi, kuralı ve kuyruğu kiracıya aittir (`Adm_T_NotificationType`,
`Adm_T_NotificationRule`, `Adm_T_Notification`; üçü de `IMultiTenant`) ve ekranların tamamı
Administration › Bildirimler altındadır. Ayrı bir manuel gönderim tablosu yoktur: **Bildirimler**
ekranının ekleme formu elle gönderimdir, uç alıcı × kanal açılımını kuyruğa yazar
(`list-form-dynamic-api/notification-insert`). Yeni kiracıya varsayılan tür/kural kümesi
kopyalanır (`NotificationTenantDefaultsSeedContributor`); kaynak host kapsamındaki seed
dosyalarıdır, satırlar yeni kimliklerle yazılır. Kiracı verisine dokunan zamanlanmış işin
`Scope` alanı `Tenant` ya da `Host ve Kiracılar` olmalıdır — `Host` kapsamında çalışan bir iş
kiracı satırlarını göremez (`WorkerScopeEnum`, `PlatformBackgroundWorker`).
- **Bildirim bayrakları** → sıra değişmez: `IsSent` (iletildi) olmadan `IsRead` yazılmaz, `IsRead`
ile `ReadTime` birlikte yazılır. `UiActivity` kayıt oluşurken iletilmiş sayılır, `UiToast`/`Desktop`
tarayıcıda gösterildiğinde (`NotificationChannels.BrowserChannels`; `sent-many` ucu yalnızca bu

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@
"Name": "Session Cleanup Worker",
"Cron": "*/5 * * * *",
"WorkerType": 4,
"Scope": 1,
"BeforeSp": null,
"AfterSp": null,
"IsActive": true
@ -17,6 +18,7 @@
"Name": "Notification Worker",
"Cron": "*/5 * * * *",
"WorkerType": 3,
"Scope": 3,
"BeforeSp": null,
"AfterSp": null,
"IsActive": true
@ -26,6 +28,7 @@
"Name": "Backup Databases",
"Cron": "0 1 * * *",
"WorkerType": 5,
"Scope": 1,
"BeforeSp": "Adm_T_DatabaseBackupAll",
"AfterSp": "Adm_T_DatabaseBackupFilesDeleteAll",
"IsActive": true
@ -35,6 +38,7 @@
"Name": "AI Save DbTableColumns",
"Cron": "0 3 * * *",
"WorkerType": 2,
"Scope": 1,
"BeforeSp": "Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns",
"AfterSp": null,
"IsActive": true

View file

@ -401,55 +401,11 @@
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Saas",
"Code": "App.Notifications",
"DisplayName": "App.Notifications",
"Order": 10,
"Url": null,
"Icon": "FcWorkflow",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Notifications",
"Code": "App.Notifications.NotificationTypes",
"DisplayName": "App.Notifications.NotificationTypes",
"Order": 1,
"Url": "/admin/list/App.Notifications.NotificationTypes",
"Icon": "FcComboChart",
"RequiredPermissionName": "App.Notifications.NotificationTypes",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Notifications",
"Code": "App.Notifications.NotificationRules",
"DisplayName": "App.Notifications.NotificationRules",
"Order": 2,
"Url": "/admin/list/App.Notifications.NotificationRules",
"Icon": "FcLibrary",
"RequiredPermissionName": "App.Notifications.NotificationRules",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Notifications",
"Code": "App.Notifications.Notification",
"DisplayName": "App.Notifications.Notification",
"Order": 3,
"Url": "/admin/list/App.Notifications.Notification",
"Icon": "FcBrokenLink",
"RequiredPermissionName": "App.Notifications.Notification",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Saas",
"Code": "App.BackgroundWorkers",
"DisplayName": "App.BackgroundWorkers",
"Order": 11,
"Order": 9,
"Url": null,
"Icon": "FcSynchronize",
"RequiredPermissionName": null,
@ -482,7 +438,7 @@
"ParentCode": "App.Saas",
"Code": "App.Menus",
"DisplayName": "App.Menus",
"Order": 12,
"Order": 10,
"Url": null,
"Icon": "FcParallelTasks",
"RequiredPermissionName": null,
@ -537,7 +493,7 @@
"ParentCode": "App.Saas",
"Code": "App.DeveloperKit",
"DisplayName": "App.DeveloperKit",
"Order": 15,
"Order": 11,
"Url": null,
"Icon": "FcAndroidOs",
"RequiredPermissionName": null,
@ -647,7 +603,7 @@
"ParentCode": "App.Saas",
"Code": "App.ForumManagement",
"DisplayName": "App.ForumManagement",
"Order": 16,
"Order": 12,
"Url": null,
"Icon": "FcReading",
"RequiredPermissionName": "App.ForumManagement",
@ -799,12 +755,34 @@
},
{
"ParentCode": "App.Administration.Notifications",
"Code": "App.ManualNotification",
"DisplayName": "App.ManualNotification",
"Code": "App.Notifications.NotificationTypes",
"DisplayName": "App.Notifications.NotificationTypes",
"Order": 2,
"Url": "/admin/list/App.ManualNotification",
"Url": "/admin/list/App.Notifications.NotificationTypes",
"Icon": "FcComboChart",
"RequiredPermissionName": "App.Notifications.NotificationTypes",
"IsDisabled": false,
"ModuleId": "Adm"
},
{
"ParentCode": "App.Administration.Notifications",
"Code": "App.Notifications.NotificationRules",
"DisplayName": "App.Notifications.NotificationRules",
"Order": 3,
"Url": "/admin/list/App.Notifications.NotificationRules",
"Icon": "FcLibrary",
"RequiredPermissionName": "App.Notifications.NotificationRules",
"IsDisabled": false,
"ModuleId": "Adm"
},
{
"ParentCode": "App.Administration.Notifications",
"Code": "App.Notifications.Notification",
"DisplayName": "App.Notifications.Notification",
"Order": 4,
"Url": "/admin/list/App.Notifications.Notification",
"Icon": "FcSms",
"RequiredPermissionName": "App.ManualNotification",
"RequiredPermissionName": "App.Notifications.Notification",
"IsDisabled": false,
"ModuleId": "Adm"
},
@ -907,22 +885,11 @@
"IsDisabled": false,
"ModuleId": "Adm"
},
{
"ParentCode": "App.Administration.Intranet",
"Code": "App.Intranet.Events.EventComment",
"DisplayName": "App.Intranet.Events.EventComment",
"Order": 7,
"Url": "/admin/list/App.Intranet.Events.EventComment",
"Icon": "FcFeedback",
"RequiredPermissionName": "App.Intranet.Events.EventComment",
"IsDisabled": false,
"ModuleId": "Adm"
},
{
"ParentCode": "App.Administration.Intranet",
"Code": "App.Intranet.Document",
"DisplayName": "App.Intranet.Document",
"Order": 8,
"Order": 7,
"Url": "/admin/intranet-documents",
"Icon": "FcOpenedFolder",
"RequiredPermissionName": "App.Intranet.Document",

View file

@ -1,4 +1,4 @@
DO $$
DO $$
BEGIN
IF to_regclass('hangfire.job') IS NULL
OR to_regclass('hangfire.jobparameter') IS NULL
@ -11,7 +11,14 @@ BEGIN
CREATE OR REPLACE VIEW public."Sas_H_BackgroundWorker_JobFlow" AS
SELECT
j.id AS "Id",
COALESCE(p."RecurringJobId", 'OneTimeJob') AS "JobName",
-- Kiraci kapsamli isin adi WorkerType:Name#TenantId biciminde kurulur; ekranda is adi sade
-- kalsin, kiraci ayri kolonda gorunsun diye ek burada ayrilir.
CASE
WHEN position('#' in COALESCE(p."RecurringJobId", '')) > 0
THEN split_part(p."RecurringJobId", '#', 1)
ELSE COALESCE(p."RecurringJobId", 'OneTimeJob')
END AS "JobName",
t."Name" AS "Tenant",
COALESCE(prc."ProcessingAt", enq."EnqueuedAt", j.createdat) AS "ExecutionDate",
CASE
WHEN suc."DurationMs" IS NOT NULL THEN suc."DurationMs"
@ -34,8 +41,6 @@ SELECT
fld."ExceptionType",
fld."ExceptionDetails"
FROM hangfire.job j
LEFT JOIN hangfire.jobqueue jq
ON jq.jobid = j.id
LEFT JOIN LATERAL
(
SELECT
@ -49,9 +54,7 @@ LEFT JOIN LATERAL
LEFT JOIN LATERAL
(
SELECT
s.createdat AS "EnqueuedAt",
s.reason AS "EnqueuedReason",
s.data::jsonb ->> 'Queue' AS "QueueName"
s.createdat AS "EnqueuedAt"
FROM hangfire.state s
WHERE s.jobid = j.id
AND s.name = 'Enqueued'
@ -61,10 +64,7 @@ LEFT JOIN LATERAL
LEFT JOIN LATERAL
(
SELECT
s.createdat AS "ProcessingAt",
s.reason AS "ProcessingReason",
s.data::jsonb ->> 'ServerId' AS "ServerName",
s.data::jsonb ->> 'WorkerId' AS "WorkerId"
s.createdat AS "ProcessingAt"
FROM hangfire.state s
WHERE s.jobid = j.id
AND s.name = 'Processing'
@ -75,7 +75,6 @@ LEFT JOIN LATERAL
(
SELECT
s.createdat AS "SucceededAt",
s.reason AS "SucceededReason",
NULLIF(s.data::jsonb ->> 'PerformanceDuration', '')::bigint AS "DurationMs"
FROM hangfire.state s
WHERE s.jobid = j.id
@ -97,6 +96,16 @@ LEFT JOIN LATERAL
ORDER BY s.createdat DESC
LIMIT 1
) fld ON TRUE
LEFT JOIN LATERAL
(
SELECT CASE
WHEN length(split_part(p."RecurringJobId", '#', 2)) = 36
AND split_part(p."RecurringJobId", '#', 2) ~ '^[0-9a-fA-F-]+$'
THEN split_part(p."RecurringJobId", '#', 2)::uuid
END AS "TenantId"
) tn ON TRUE
LEFT JOIN public."AbpTenants" t
ON t."Id" = tn."TenantId"
WHERE COALESCE(j.invocationdata::jsonb ->> 'Method', '') <> 'DoWorkAsync'
$view$;
END;

View file

@ -1,9 +1,17 @@
CREATE OR ALTER VIEW [dbo].[Sas_H_BackgroundWorker_JobFlow] AS
CREATE OR ALTER VIEW [dbo].[Sas_H_BackgroundWorker_JobFlow] AS
SELECT
J.Id,
ISNULL(P.RecurringJobId, N'OneTimeJob') AS [JobName],
-- Kiraci kapsamli isin adi WorkerType:Name#TenantId biciminde kurulur; ekranda is adi sade
-- kalsin, kiraci ayri kolonda gorunsun diye ek burada ayrilir.
CASE
WHEN CHARINDEX(N'#', ISNULL(P.RecurringJobId, N'')) > 0
THEN LEFT(P.RecurringJobId, CHARINDEX(N'#', P.RecurringJobId) - 1)
ELSE ISNULL(P.RecurringJobId, N'OneTimeJob')
END AS [JobName],
T.Name AS [Tenant],
COALESCE(PRC.ProcessingAt, ENQ.EnqueuedAt, J.CreatedAt) AS [ExecutionDate],
@ -38,9 +46,6 @@ SELECT
FROM HangFire.Job J
LEFT JOIN HangFire.JobQueue JQ
ON JQ.JobId = J.Id
OUTER APPLY
(
SELECT
@ -55,9 +60,7 @@ OUTER APPLY
OUTER APPLY
(
SELECT TOP 1
S.CreatedAt AS EnqueuedAt,
S.Reason AS EnqueuedReason,
JSON_VALUE(S.Data, '$.Queue') AS QueueName
S.CreatedAt AS EnqueuedAt
FROM HangFire.State S
WHERE S.JobId = J.Id
AND S.Name = 'Enqueued'
@ -67,10 +70,7 @@ OUTER APPLY
OUTER APPLY
(
SELECT TOP 1
S.CreatedAt AS ProcessingAt,
S.Reason AS ProcessingReason,
JSON_VALUE(S.Data, '$.ServerId') AS ServerName,
JSON_VALUE(S.Data, '$.WorkerId') AS WorkerId
S.CreatedAt AS ProcessingAt
FROM HangFire.State S
WHERE S.JobId = J.Id
AND S.Name = 'Processing'
@ -81,7 +81,6 @@ OUTER APPLY
(
SELECT TOP 1
S.CreatedAt AS SucceededAt,
S.Reason AS SucceededReason,
TRY_CONVERT(BIGINT, JSON_VALUE(S.Data, '$.PerformanceDuration')) AS DurationMs
FROM HangFire.State S
WHERE S.JobId = J.Id
@ -103,5 +102,17 @@ OUTER APPLY
ORDER BY S.CreatedAt DESC
) FLD
OUTER APPLY
(
SELECT TRY_CONVERT(UNIQUEIDENTIFIER,
CASE
WHEN CHARINDEX(N'#', ISNULL(P.RecurringJobId, N'')) > 0
THEN SUBSTRING(P.RecurringJobId, CHARINDEX(N'#', P.RecurringJobId) + 1, 36)
END) AS TenantId
) TN
LEFT JOIN dbo.AbpTenants T
ON T.Id = TN.TenantId
WHERE JSON_VALUE(J.InvocationData, '$.Method') <> 'DoWorkAsync'
GO