Fable5 bakışı ile düzenlemeler

This commit is contained in:
Sedat Öztürk 2026-10-17 21:51:10 +03:00
parent a56bdc3e79
commit 5e8ee03fc8
538 changed files with 11418 additions and 12315 deletions

View file

@ -129,7 +129,13 @@ Ek kurallar:
ile çevirir. Çeviride karşılığı olmayan bir sözdizimi kullanılırsa `PostgreSqlModelSyntax`'a ile çevirir. Çeviride karşılığı olmayan bir sözdizimi kullanılırsa `PostgreSqlModelSyntax`'a
eklenir; immutable olmayan hesaplanan kolon (`GETDATE()`'e bağlı) PG'de yazılamaz, sağlayıcıya eklenir; immutable olmayan hesaplanan kolon (`GETDATE()`'e bağlı) PG'de yazılamaz, sağlayıcıya
göre dallanır. C#'ta üretilen SQL tırnaklı tanımlayıcı ve ANSI ifadelerle yazılır ya da göre dallanır. C#'ta üretilen SQL tırnaklı tanımlayıcı ve ANSI ifadelerle yazılır ya da
`DataSourceType`'a göre dallanır; yalnızca T-SQL'de çalışan metin eklenmez. `DataSourceType`'a göre dallanır; yalnızca T-SQL'de çalışan metin eklenmez. Tanımlayıcı
doğrulama/tırnaklama ve varsayılan şema tek yerdedir: `Sozsoft.Platform.Data.SqlIdentifier`
(Domain.Shared; `IsSafe`, `Quote`, `QuoteQualified`, `DefaultSchema`), nesne kataloğu sorgusu
`SqlCatalogQueries`; ikinci bir regex/`"public" : "dbo"` kopyası yazılmaz. Dapper deposu tek
gerçeklemedir (`DynamicDataRepository`, sağlayıcı `DbProviderFactory` ile seçilir; `Ms`/`Pg`
anahtarlı sınıflar yalnızca DI kaydıdır). `PropertyNameCaseInsensitive` JSON okuması
`PlatformJsonOptions.CaseInsensitive` ile yapılır, çağrı başına `JsonSerializerOptions` üretilmez.
- **Mapping**: Mapperly (`[Mapper]` partial class) kullanılır; AutoMapper eklenmez. - **Mapping**: Mapperly (`[Mapper]` partial class) kullanılır; AutoMapper eklenmez.
Mapper'lar servis klasöründeki `{Alan}Mappers.cs` dosyasındadır (modülde Mapper'lar servis klasöründeki `{Alan}Mappers.cs` dosyasındadır (modülde
`{Modül}ApplicationMappers.cs`), ABP'nin `MapperBase<TSource, TDest>` / `{Modül}ApplicationMappers.cs`), ABP'nin `MapperBase<TSource, TDest>` /

View file

@ -998,7 +998,7 @@ Bir kolonu **başka bir tablodan** çekmek için; view yazmadan tek alanlık joi
| `SelectFieldName` | Ekranda gösterilecek alan | | `SelectFieldName` | Ekranda gösterilecek alan |
| `FieldNameAlias` | Alanın takma adı — doluysa kolon sorgudan **bu adla** döner; grid sütunu `FieldName`'e bağlı olduğu için boş kalır. Ekranda gösterilecek join alanında boş bırak | | `FieldNameAlias` | Alanın takma adı — doluysa kolon sorgudan **bu adla** döner; grid sütunu `FieldName`'e bağlı olduğu için boş kalır. Ekranda gösterilecek join alanında boş bırak |
| `JoinCondition2` | İkinci koşulun bağlacı (`AND` varsayılan \| `OR`) | | `JoinCondition2` | İkinci koşulun bağlacı (`AND` varsayılan \| `OR`) |
| `JoinFieldName2` | İkinci koşulun alanı: `Def_T_City.CultureName` ya da yalın `CultureName` (join tablosuna bağlanır); parçalar ayrı ayrı tırnaklanır | | `JoinFieldName2` | İkinci koşulun alanı: `Sas_H_City.CultureName` ya da yalın `CultureName` (join tablosuna bağlanır); parçalar ayrı ayrı tırnaklanır |
| `BaseTableJoinFieldName2_OrValue` | İkinci koşulun karşı tarafı: **ham** yazılır (tırnaklanmaz) — tablo adlı alan kendi tırnağıyla, sabit `'en'` gibi | | `BaseTableJoinFieldName2_OrValue` | İkinci koşulun karşı tarafı: **ham** yazılır (tırnaklanmaz) — tablo adlı alan kendi tırnağıyla, sabit `'en'` gibi |
Aynı JOIN cümlesi birden çok alanda geçerse bir kez yazılır; join alanına filtre/sıralama Aynı JOIN cümlesi birden çok alanda geçerse bir kez yazılır; join alanına filtre/sıralama
@ -1393,7 +1393,7 @@ işler `data/App.BackgroundWorkers.RecurringJobs.json` ile gelir.
| --- | --- | | --- | --- |
| `MailType` | Şablon tipi etiketi | | `MailType` | Şablon tipi etiketi |
| `MailSubject` | Mail konusu | | `MailSubject` | Mail konusu |
| `MailTemplate` | Razor ile render edilen HTML gövde (`EmailLayout` düzeni içinde) | | `MailTemplate` | Razor ile render edilen HTML gövde (`EmailLayout` düzeni içinde). Şablon işin adıyla `PlatformTemplateDynamicStore` üzerinden istendiği anda okunur; seçenek değişince yeniden başlatma gerekmez |
| `TableName` | Maildeki tabloların tarifi (§11.2) | | `TableName` | Maildeki tabloların tarifi (§11.2) |
### 11.2 `TableName` biçimi ### 11.2 `TableName` biçimi
@ -1415,9 +1415,11 @@ birden fazlası `};{` ile birleştirilir:
### 11.3 Kuyruk satırı (`Sas_H_BackgroundWorker_MailQueue`) ### 11.3 Kuyruk satırı (`Sas_H_BackgroundWorker_MailQueue`)
`TemplateId` (worker), `From`, `To`, `MailParameter`, `TableName`, `TableParameter`, `TemplateId` (worker), `From`, `To`, `MailParameter`, `TableName`, `TableParameter`,
`Attachment`, `AttachmentParameter`, `RelatedRecordId`, `SendStatus`, `SendTime`, `AwsMessageId`. `RelatedRecordId`, `SendStatus`, `SendTime`, `AwsMessageId`. `Attachment` ve `AttachmentParameter`
Gönderim sonuçları `Sas_H_BackgroundWorker_MailQueueEvents` tablosunda izlenir. Tabloların kolonları eski MS Access rapor eki yolundan kalmadır ve **okunmaz**; mail eki yalnızca
ekranı yoktur; satırı kanca/prosedür yazar. `TableName` içindeki `PDF`/`XLS`/`TXT` tablolarından üretilir (§11.2). Gönderim sonuçları
`Sas_H_BackgroundWorker_MailQueueEvents` tablosunda izlenir. Tabloların ekranı yoktur; satırı
kanca/prosedür yazar.
Kuyruğa satır yazma işi genellikle worker'ın `BeforeSp` prosedüründe ya da ekranın Kuyruğa satır yazma işi genellikle worker'ın `BeforeSp` prosedüründe ya da ekranın
`InsertAfterCommand` kancasında yapılır — yani "kayıt onaylanınca mail çıksın" isteği kod `InsertAfterCommand` kancasında yapılır — yani "kayıt onaylanınca mail çıksın" isteği kod
@ -1452,7 +1454,7 @@ Platform tablolarının adı `TableNameResolver` tarafından üretilir:
| Ad | `TableNameEnum` üyesi | | Ad | `TableNameEnum` üyesi |
Örnek: `Sas_H_Currency` (host — tüm kiracılar aynı para birimlerini görür), `Hr_T_Department` Örnek: `Sas_H_Currency` (host — tüm kiracılar aynı para birimlerini görür), `Hr_T_Department`
(kiracı başına), `V_Sas_H_Order` (view). Menü ile tablo öneki her zaman örtüşmez: (kiracı başına), `Hr_V_LeaveSummary` (view — `{Modül}_V_{Ad}`). Menü ile tablo öneki her zaman örtüşmez:
`Sas_T_GlobalSearch`, `Sas_T_Branch`, `Sas_T_Forum*`, `Sas_T_IntranetWidget` kiracı tablosudur. Özel view'ler `Sas_T_GlobalSearch`, `Sas_T_Branch`, `Sas_T_Forum*`, `Sas_T_IntranetWidget` kiracı tablosudur. Özel view'ler
`TableNameResolver.GetFullSpecialViewName` ile `V_{Önek}_{Prefix}_{Ad}` biçiminde üretilir. `TableNameResolver.GetFullSpecialViewName` ile `V_{Önek}_{Prefix}_{Ad}` biçiminde üretilir.
@ -1620,7 +1622,7 @@ listesinde `Hr` varsa görür (modül listesi boş kiracıda süzme yoktur — `
| **Intranet › Announcement** | `App.Hr.Intranet.Announcement` (alt formlar Like/Comment) | `Hr_T_Announcement` · `Hr_T_AnnouncementLike` · `Hr_T_AnnouncementComment` | tenant | Duyuru + beğeni + yorum: **ana kayıt / alt etkileşim** kalıbının hazır örneği. | | **Intranet › Announcement** | `App.Hr.Intranet.Announcement` (alt formlar Like/Comment) | `Hr_T_Announcement` · `Hr_T_AnnouncementLike` · `Hr_T_AnnouncementComment` | tenant | Duyuru + beğeni + yorum: **ana kayıt / alt etkileşim** kalıbının hazır örneği. |
| **Intranet › Survey** | `App.Hr.Intranet.Survey` (alt form Question → Option; Response → Answer) | `Hr_T_Survey` · `Hr_T_SurveyQuestion` · `Hr_T_SurveyQuestionOption` · `Hr_T_SurveyResponse` · `Hr_T_SurveyAnswer` | tenant | Anket motoru; soru–seçenek–yanıt zinciri **iki seviyeli alt form** ihtiyacının referansıdır. | | **Intranet › Survey** | `App.Hr.Intranet.Survey` (alt form Question → Option; Response → Answer) | `Hr_T_Survey` · `Hr_T_SurveyQuestion` · `Hr_T_SurveyQuestionOption` · `Hr_T_SurveyResponse` · `Hr_T_SurveyAnswer` | tenant | Anket motoru; soru–seçenek–yanıt zinciri **iki seviyeli alt form** ihtiyacının referansıdır. |
| **Intranet › Social Post** | `App.Hr.Intranet.SocialPost` | `Hr_T_SocialPost` · `Hr_T_SocialComment` · `Hr_T_SocialLike` (+ `SocialPollOption`, `SocialMedia`, `SocialLocation`) | tenant | Kurum içi akış; `IsLiked`, `IsOwnPost` gibi hesaplanmış kolonların view içinde üretilmesine örnek. Alt formlar: `App.Hr.Intranet.SocialMedia`, `SocialComment`, `SocialLike` (`Id` → `SocialPostId`); anket seçenekleri (`App.Hr.Intranet.SocialPollOption`) medyaya bağlı olduğu için `SocialMedia` ekranının alt formudur (`Id` → `SocialMediaId`). | | **Intranet › Social Post** | `App.Hr.Intranet.SocialPost` | `Hr_T_SocialPost` · `Hr_T_SocialComment` · `Hr_T_SocialLike` (+ `SocialPollOption`, `SocialMedia`, `SocialLocation`) | tenant | Kurum içi akış; `IsLiked`, `IsOwnPost` gibi hesaplanmış kolonların view içinde üretilmesine örnek. Alt formlar: `App.Hr.Intranet.SocialMedia`, `SocialComment`, `SocialLike` (`Id` → `SocialPostId`); anket seçenekleri (`App.Hr.Intranet.SocialPollOption`) medyaya bağlı olduğu için `SocialMedia` ekranının alt formudur (`Id` → `SocialMediaId`). |
| **Intranet › Event Type / Category / Event** | `App.Hr.Intranet.Events.EventType` · `.EventCategory` · `.Event` | `Hr_T_EventType` · `Hr_T_EventCategory` · `Hr_T_Event` · `Hr_T_EventPhoto` · `Hr_T_EventLike` · `Hr_T_EventComment` | tenant | Etkinlik yönetimi; foto ve katılımcı sayacı olan kayıt kalıbı. `EventLike`/`EventComment` alt formdur, `EventPhoto` menüde yer almaz; yetkileri `App.Hr.Intranet.Events.Event` altındadır. `Hr_T_EventPhoto` tablosu `sql/object/Hr_T_EventPhoto.sql` ile oluşturulur. | | **Intranet › Event Type / Category / Event** | `App.Hr.Intranet.Events.EventType` · `.EventCategory` · `.Event` | `Hr_T_EventType` · `Hr_T_EventCategory` · `Hr_T_Event` · `Hr_T_EventLike` · `Hr_T_EventComment` | tenant | Etkinlik yönetimi; foto ve katılımcı sayacı olan kayıt kalıbı. `EventLike`/`EventComment` alt formdur; yetkileri `App.Hr.Intranet.Events.Event` altındadır. Ayrı bir fotoğraf tablosu yoktur: fotoğraflar `Hr_T_Event.Photos` kolonunda tutulur (bkz. `docs/modules/App.Hr.md`). |
| **Intranet › Documents** | `App.Hr.Intranet.Document` · `/admin/intranet-documents` | Dosya yöneticisi (`intranet` / `intranet\|_passive` klasörleri) | tenant | Kurum içi doküman paylaşımı — aktif/pasif klasör, yükleme, taşıma, toplu silme. Doküman listesi isteği yeni tablo gerektirmez. | | **Intranet › Documents** | `App.Hr.Intranet.Document` · `/admin/intranet-documents` | Dosya yöneticisi (`intranet` / `intranet\|_passive` klasörleri) | tenant | Kurum içi doküman paylaşımı — aktif/pasif klasör, yükleme, taşıma, toplu silme. Doküman listesi isteği yeni tablo gerektirmez. |
**Şube bilgisi:** `Department.BranchId` ve `JobPosition.BranchId` (`Guid`, **zorunlu** — kolon `NOT NULL`, formda **Şube bilgisi:** `Department.BranchId` ve `JobPosition.BranchId` (`Guid`, **zorunlu** — kolon `NOT NULL`, formda

View file

@ -2237,7 +2237,7 @@ Düzenleme tabloya hiç dokunmaz.
bırakılan (`Kept`) satırın sahibi silinen wizard'dı ve sonradan hiçbir silme onu kaldırmaz. bırakılan (`Kept`) satırın sahibi silinen wizard'dı ve sonradan hiçbir silme onu kaldırmaz.
1. **Alt formlar önce, onları taşıyan ekran sonra** — en derindeki alt form ilk gider 1. **Alt formlar önce, onları taşıyan ekran sonra** — en derindeki alt form ilk gider
(`HealthCardWorkHistory` → `HealthCardGeneral` → … → `HealthCard`). Bağlı wizard'ı olan wizard (`Hr_HealthCardHistory` → `Hr_HealthCardResult` → … → `Hr_HealthCard`). Bağlı wizard'ı olan wizard
zaten silinemez (yukarıda). zaten silinemez (yukarıda).
2. **Aynı seviyede en son deploy edilen önce** — paylaşılan menü seviyesi, ara yetki, izin grubu ve 2. **Aynı seviyede en son deploy edilen önce** — paylaşılan menü seviyesi, ara yetki, izin grubu ve
dil anahtarı onu **ilk** üreten wizard'a aittir; o wizard, diğerleri gidip satır kullanımsız dil anahtarı onu **ilk** üreten wizard'a aittir; o wizard, diğerleri gidip satır kullanımsız
@ -2258,7 +2258,7 @@ işlemde siler. Onay diyaloğu sırayı ve tabloları gösterir:
yüzünden korunmaz (`WizardRecordCleaner.CleanAsync(seed, batchListForms)`); tablo kullanımında da yüzünden korunmaz (`WizardRecordCleaner.CleanAsync(seed, batchListForms)`); tablo kullanımında da
talebin ekranları sayılmaz (`WizardTableCleaner.AnalyzeAsync(seed, batchListForms)`). talebin ekranları sayılmaz (`WizardTableCleaner.AnalyzeAsync(seed, batchListForms)`).
- **Tablolar en sonda** düşürülür: bütün ekranlar gittikten sonra kullanım yeniden ölçülür; aynı - **Tablolar en sonda** düşürülür: bütün ekranlar gittikten sonra kullanım yeniden ölçülür; aynı
tabloyu paylaşan alt formlar (`Hr_T_HealthCard` gibi) tabloyu bir kez listeler. Bağlantılar veri tabloyu paylaşan alt formlar (`Inv_T_Transfer`'ı paylaşan `TransferOther`/`TransferNote` gibi) tabloyu bir kez listeler. Bağlantılar veri
kaynağı kaydı silinmeden önce çözülür. Tablo seçmek yine `App.SqlQueryManager` ister. kaynağı kaydı silinmeden önce çözülür. Tablo seçmek yine `App.SqlQueryManager` ister.
- **Talebin Custom Endpoint'leri de gider:** tablo talebe aitse (`create_table`/tasarımcı scripti var, - **Talebin Custom Endpoint'leri de gider:** tablo talebe aitse (`create_table`/tasarımcı scripti var,
talep dışında ekran kullanmıyor) onu sorgulayan Custom Endpoint'ler tabloyu tutmaz; veritabanından talep dışında ekran kullanmıyor) onu sorgulayan Custom Endpoint'ler tabloyu tutmaz; veritabanından
@ -2807,12 +2807,12 @@ adıma döner.
**Adım 2 — Menü.** `WizardName` her yolda zorunludur. Wizard adı seed dosyasının adıdır ve bütün **Adım 2 — Menü.** `WizardName` her yolda zorunludur. Wizard adı seed dosyasının adıdır ve bütün
modüllerde tekil olmak zorundadır; aynı ekran adı birden çok modülde geçebilir (İK ve Satınalma modüllerde tekil olmak zorundadır; aynı ekran adı birden çok modülde geçebilir (İK ve Satınalma
altında `RejectionReason`). Bu yüzden yeni wizard'ın adı modül önekini taşır: `{ModuleId}_{Ad}` altında `RejectionReason`). Bu yüzden yeni wizard'ın adı modül önekini taşır: `{ModuleId}_{Ad}`
(`Hr_RejectionReason`, `Prc_RejectionReason`, `Inventory_Warehouse`). Önek `MenuParentModuleId`'den (`Hr_RejectionReason`, `Prc_RejectReason`, `Inventory_Warehouse`). Önek `MenuParentModuleId`'den
gelir ve deploy'da eklenir (`ListFormWizardDto.ResolveWizardName`); ad zaten önekle başlıyorsa gelir ve deploy'da eklenir (`ListFormWizardDto.ResolveWizardName`); ad zaten önekle başlıyorsa
tekrar eklenmez. `configs/seeds/wizard/` altındaki bütün dosyalar bu düzendedir; öneksiz ad yalnızca tekrar eklenmez. `configs/seeds/wizard/` altındaki bütün dosyalar bu düzendedir; öneksiz ad yalnızca
eski bir export arşivinden gelebilir ve o wizard düzenlenirken adı korunur. Ekran, menü ve eski bir export arşivinden gelebilir ve o wizard düzenlenirken adı korunur. Ekran, menü ve
yetki kodları (`ListFormCode`, `MenuCode`) önekten etkilenmez. Üretim planında ve `replaceFileName`'de yetki kodları (`ListFormCode`, `MenuCode`) önekten etkilenmez. Üretim planında ve `replaceFileName`'de
dosya adı önekli yazılır (`deploy_wizard: Prc_RejectionReason.json`). `CreateMenu` anahtarı kapatılırsa menü dosya adı önekli yazılır (`deploy_wizard: Prc_RejectReason.json`). `CreateMenu` anahtarı kapatılırsa menü
alanlarının tamamı gizlenir ve menü/üst menü kaydı üretilmez (alt form ekranı kalıbı, §4.6). alanlarının tamamı gizlenir ve menü/üst menü kaydı üretilmez (alt form ekranı kalıbı, §4.6).
Açıkken zorunlular: üst menü (ağaçtan seçilir ya da yeni kök tanımlanır), `MenuCode`, EN+TR menü Açıkken zorunlular: üst menü (ağaçtan seçilir ya da yeni kök tanımlanır), `MenuCode`, EN+TR menü
etiketi, izin grubu (listeden seçilir ya da yeni ad; görünen adları EN+TR yazılır), `MenuIcon` etiketi, izin grubu (listeden seçilir ya da yeni ad; görünen adları EN+TR yazılır), `MenuIcon`
@ -3200,7 +3200,7 @@ DevExtreme editörüne (ve platformun kendi editörlerine) geçirilen JSON. UI s
| --- | --- | --- | | --- | --- | --- |
| `boolean` | üç durumlu seçim | `true` / `false` / (yok) | | `boolean` | üç durumlu seçim | `true` / `false` / (yok) |
| `number` | sayı | `12` | | `number` | sayı | `12` |
| `text` | metin | `"dd/MM/yyyy"` | | `text` | metin | `"shortDate"` |
| `select` | listeden seçim (bilinmeyen mevcut değer korunur) | seçilen değer | | `select` | listeden seçim (bilinmeyen mevcut değer korunur) | seçilen değer |
| `size` | sayı ya da CSS | `200` veya `"100%"` | | `size` | sayı ya da CSS | `200` veya `"100%"` |
| `stringList` | virgülle ayrılmış | `["key","name"]` | | `stringList` | virgülle ayrılmış | `["key","name"]` |
@ -3242,8 +3242,8 @@ Preset mevcut JSON ile **birleşir**, diğer ayarları silmez; aynı anahtar var
| Salt okunur / Pasif | `{"readOnly":true}` · `{"disabled":true}` | | Salt okunur / Pasif | `{"readOnly":true}` · `{"disabled":true}` |
| Sağa hizalı | `{"inputAttr":{"style":"text-align: right"}}` | | Sağa hizalı | `{"inputAttr":{"style":"text-align: right"}}` |
| 2 ondalık · Artırmalı sayı · Para | `format.fixedPoint(2)` · + `useMaskBehavior` + `showSpinButtons` · `format.currency TRY` | | 2 ondalık · Artırmalı sayı · Para | `format.fixedPoint(2)` · + `useMaskBehavior` + `showSpinButtons` · `format.currency TRY` |
| Tarih · Tarih-saat · Saat | `type`+`displayFormat`+`dateSerializationFormat` üçlüsü (saatte ayrıca `pickerType:list`, `interval`, `width:100%`) | | Tarih · Tarih-saat · Saat | `type`+`displayFormat`+`dateSerializationFormat` üçlüsü (saatte ayrıca `pickerType:list`, `interval`, `width:100%`). `displayFormat` DevExtreme'in adlı biçimidir — `shortDate` · `shortDateShortTime` · `shortTime` — aktif dilin düzeniyle çizilir; `dd/MM/yyyy` gibi sabit desen yazılmaz |
| Telefon maskesi | `mask`, `maskRules.X`, `maskInvalidMessage`, `placeholder`, `useMaskedValue:false` — token değil sabit maskedir; `phoneGlobal` token'ının preset'i yoktur, `format` alanına elle yazılır | | Telefon maskesi | `mask`, `maskRules.X`, `maskInvalidMessage` (dil anahtarı, ör. `App.PresetPhoneMask.InvalidMessage`; liste/form aktif dile çevirir), `placeholder`, `useMaskedValue:false` — token değil sabit maskedir; `phoneGlobal` token'ının preset'i yoktur, `format` alanına elle yazılır |
| Telefon (Türkiye) · (uluslararası) · (ülkeye göre) | `{"format":"phoneTR"}` · `{"format":"phoneIntl"}` · `{"format":"phoneCountry","phoneCountryField":"Country"}` — üçü de `showClearButton:true` ekler; ayrıntı §6.4.1 | | Telefon (Türkiye) · (uluslararası) · (ülkeye göre) | `{"format":"phoneTR"}` · `{"format":"phoneIntl"}` · `{"format":"phoneCountry","phoneCountryField":"Country"}` — üçü de `showClearButton:true` ekler; ayrıntı §6.4.1 |
| Otomatik büyüyen metin | `autoResizeEnabled`, `minHeight:80`, `maxHeight:320` | | Otomatik büyüyen metin | `autoResizeEnabled`, `minHeight:80`, `maxHeight:320` |
| Aranabilir liste | `searchEnabled`, `searchMode:contains`, `searchTimeout`, `showClearButton` | | Aranabilir liste | `searchEnabled`, `searchMode:contains`, `searchTimeout`, `showClearButton` |
@ -3297,7 +3297,7 @@ varsayılanı ezer.
| Çok satırlı | `EditorOptions.Multiline(80)` | `{"height":80}` | | Çok satırlı | `EditorOptions.Multiline(80)` | `{"height":80}` |
| Sabit ondalık | `EditorOptions.Number(2)` | format + maske davranışı + spin | | Sabit ondalık | `EditorOptions.Number(2)` | format + maske davranışı + spin |
| Yüzde | `EditorOptions.Percent()` | `{"format":"#0.##'%'","useMaskBehavior":true,"showSpinButtons":true}` | | Yüzde | `EditorOptions.Percent()` | `{"format":"#0.##'%'","useMaskBehavior":true,"showSpinButtons":true}` |
| Tarih / tarih-saat / saat | `EditorOptions.Date()` · `DateTime()` · `Time(15)` | Tarih/tarih-saat: `format` + `displayFormat` (UI preset'inden farklı olarak `type` ve `dateSerializationFormat` yazmaz). Saat: `type:time`, `pickerType:list`, `displayFormat:HH:mm`, `dateSerializationFormat`, `interval`, `width:100%` — UI preset'iyle aynı | | Tarih / tarih-saat / saat | `EditorOptions.Date()` · `DateTime()` · `Time(15)` | UI preset'leriyle (`date` · `dateTime` · `timePicker`) birebir aynı: `type` + `displayFormat` (`shortDate` · `shortDateShortTime` · `shortTime`, DevExtreme adlı biçim — sabit desen yok) + `dateSerializationFormat`; saatte ayrıca `pickerType:list`, `interval`, `width:100%` |
| Telefon | `EditorOptions.Phone()` | `format:"phoneGlobal"` + `mask` + `maskInvalidMessage` + `useMaskedValue:false` + `maskRules` + `placeholder` | | Telefon | `EditorOptions.Phone()` | `format:"phoneGlobal"` + `mask` + `maskInvalidMessage` + `useMaskedValue:false` + `maskRules` + `placeholder` |
| Telefon (ülkeye göre) | `EditorOptions.PhoneByCountry("Country")` | `{"format":"phoneCountry","phoneCountryField":"Country","showClearButton":true}` | | Telefon (ülkeye göre) | `EditorOptions.PhoneByCountry("Country")` | `{"format":"phoneCountry","phoneCountryField":"Country","showClearButton":true}` |
| Kaydırıcı | `EditorOptions.Slider()` | `{"tooltip":{"enabled":true}}` | | Kaydırıcı | `EditorOptions.Slider()` | `{"tooltip":{"enabled":true}}` |

View file

@ -149,6 +149,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
sözleşmesini göstermeli. sözleşmesini göstermeli.
4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü 4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü
metin önerme. metin önerme.
**Kullanıcıya görünen her tarih ve sayı** → aktif kültürle: UI'da `localeFormat.ts`
(`formatLocaleDate/Time/DateTime/Number/Currency/Fixed`), API'de `CultureFormat` + `CultureInfo.CurrentCulture`;
DevExtreme kolon/editörü adlı biçim kullanır (`shortDate`, `shortDateShortTime`, `shortTime`, `decimal`,
`fixedPoint`, `currency`). Sabit desen (`DD.MM.YYYY`, `dd/MM/yyyy`), `'tr-TR'` sabiti, `toLocaleString()` ve
görüntülenen ondalık için `toFixed` yazılmaz; makine biçimi (ISO yük, `<input type=date>`) `ui/README.md`'de.
5. **Runtime'da üretilen her artefakt** (wizard, custom component, crud endpoint) → ilgili seed 5. **Runtime'da üretilen her artefakt** (wizard, custom component, crud endpoint) → ilgili seed
dosyası da yazılır/güncellenir. Veritabanı sıfırlandığında geri gelmeyen bir şey üretme. dosyası da yazılır/güncellenir. Veritabanı sıfırlandığında geri gelmeyen bir şey üretme.
6. **Yeni ekran/komponent talebi — yazılı ya da görsel** → dosya yazmadan önce **keşif turu**: 6. **Yeni ekran/komponent talebi — yazılı ya da görsel** → dosya yazmadan önce **keşif turu**:
@ -258,12 +263,12 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
21. **Demo veri ≠ seed senkronu** → `SeedData: true` yalnızca `SeedFilePath` açar; 21. **Demo veri ≠ seed senkronu** → `SeedData: true` yalnızca `SeedFilePath` açar;
`SeedSyncInsert/Update/Delete` istekte açıkça istenmedikçe `false`'tur (wizard cevabı `SeedSync`). `SeedSyncInsert/Update/Delete` istekte açıkça istenmedikçe `false`'tur (wizard cevabı `SeedSync`).
22. **Wizard adı modül önekli** → yeni wizard'ın adı (= seed dosyasının adı) `{ModuleId}_{Ad}`'dır 22. **Wizard adı modül önekli** → yeni wizard'ın adı (= seed dosyasının adı) `{ModuleId}_{Ad}`'dır
(`Prc_RejectionReason.json`, `Hr_RejectionReason.json`); aynı ekran adı başka modülde de geçebildiği (`Prc_RejectReason.json`, `Hr_RejectionReason.json`); aynı ekran adı başka modülde de geçebildiği
için öneksiz ad dosya çakışması (`'X' adında bir Wizard zaten var`) üretir. Önek deploy'da için öneksiz ad dosya çakışması (`'X' adında bir Wizard zaten var`) üretir. Önek deploy'da
`MenuParentModuleId`'den eklenir (`ListFormWizardDto.ResolveWizardName`); analiz ve üretim planında `MenuParentModuleId`'den eklenir (`ListFormWizardDto.ResolveWizardName`); analiz ve üretim planında
dosya adı önekli yazılır. `configs/seeds/wizard/` altındaki bütün dosyalar bu düzendedir; öneksiz dosya adı önekli yazılır. `configs/seeds/wizard/` altındaki bütün dosyalar bu düzendedir; öneksiz
dosya yalnızca eski bir export arşivinden gelebilir ve düzenlenirken adı korunur. Ayrıntı: dosya yalnızca eski bir export arşivinden gelebilir ve düzenlenirken adı korunur. Ayrıntı:
`lowcode.instructions.md` §4.4 Adım 2. `lowcode.instructions.md` §4.10 (Adım 2 — Menü).
23. **Otomatik numara Sequence'tan** → wizard/MCP üretiminde talep otomatik numara istediğinde ya da 23. **Otomatik numara Sequence'tan** → wizard/MCP üretiminde talep otomatik numara istediğinde ya da
analiz bir alanın (`ProjectNo`, `OrderNo`, `RequestNo` …) otomatik numaralanmasına karar verdiğinde analiz bir alanın (`ProjectNo`, `OrderNo`, `RequestNo` …) otomatik numaralanmasına karar verdiğinde
alan `App.Definitions.Sequence` numaratörüne bağlanır; elle girilen numara/kod alanı bu kurala alan `App.Definitions.Sequence` numaratörüne bağlanır; elle girilen numara/kod alanı bu kurala
@ -312,7 +317,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **`Sequence.GetNextSequence` (C#) ↔ `Adm_P_SequenceNext.sql` (SQL Server + PG)** — numaratörün biçim - **`Sequence.GetNextSequence` (C#) ↔ `Adm_P_SequenceNext.sql` (SQL Server + PG)** — numaratörün biçim
(`FormatTemplate` token'ları, `PaddingSize`) ve sıfırlama (`ResetPeriod`/`LastPeriodKey`) kuralı iki yerde yazılır: (`FormatTemplate` token'ları, `PaddingSize`) ve sıfırlama (`ResetPeriod`/`LastPeriodKey`) kuralı iki yerde yazılır:
formdan açılan kayıt C#'tan, yordamın açtığı belge (tekliften sipariş, alternatif teklif) SQL'den numara alır. formdan açılan kayıt C#'tan, yordamın açtığı belge (tekliften sipariş, alternatif teklif) SQL'den numara alır.
Biri değişirse diğeri de değişir; yoksa aynı numaratör iki farklı biçimde numara üretir. Biri değişirse diğeri de değişir; yoksa aynı numaratör iki farklı biçimde numara üretir. Eşzamanlılık korumaları
ayrıdır ve ikisi de kalır: yordam satır kilidi (`UPDLOCK`/`FOR UPDATE`), C# karşılaştır-ve-yaz
(`ISequenceRepository.TryAdvanceAsync`, `SequenceManager` yeniden dener) — entity izlenerek okunup `UpdateAsync`
ile yazılmaz, iki form aynı numarayı alırdı.
- **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır - **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer/Rating'in (`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer/Rating'in
tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa
@ -620,8 +628,12 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
kaydın `LastModificationTime`/`CreationTime` değerinden yeniyse güncellenir/silinir. Migrate kaydın `LastModificationTime`/`CreationTime` değerinden yeniyse güncellenir/silinir. Migrate
kullanıcının runtime revizyonunu ezmez. Yeni bir seeder yazarken bu kuralı taklit et: **migrate kullanıcının runtime revizyonunu ezmez. Yeni bir seeder yazarken bu kuralı taklit et: **migrate
varsayılanı eklemektir, ezmek değil.** varsayılanı eklemektir, ezmek değil.**
- **Modül = menü = tablo öneki = yetki kökü** → kök menüler `Sas` (`App.Saas`), `Adm` - **Modül = menü = tablo öneki = yetki kökü** → platformun C# seeder'lı kök menüleri `Sas` (`App.Saas`),
(`App.Administration`), `Hr` (`App.Hr`, İK) ve `Pub` modüllerindedir. Bir ekranı modüller arası `Adm` (`App.Administration`), `Hr` (`App.Hr`, İK) ve `Pub` (`App.Public.Header`) modüllerindedir
(`TableNameResolver.MenuPrefix` yalnızca bunları bilir); wizard ile üretilen modüller kendi kök menüsü ve
tablo önekiyle gelir — `Prj` (`App.Prj`), `Prc` (`App.Prc`), `Inventory` (`App.Inventory`, `Inv_`),
`Mm` (`App.Maintenance`), `Production` (`App.Production`, `Prd_`); tam liste `data/App.Menus.Menu.json`
kök satırlarıdır. Bir ekranı modüller arası
taşımak birlikte yapılır: `TableNameResolver` (`MenuPrefix`) + `AppCodes` (Seed/Platform) + taşımak birlikte yapılır: `TableNameResolver` (`MenuPrefix`) + `AppCodes` (Seed/Platform) +
`ListFormSeeder_{Saas,Administration,Hr}.cs` + `data/{ListFormCode}.json` adı + menü/yetki/dil `ListFormSeeder_{Saas,Administration,Hr}.cs` + `data/{ListFormCode}.json` adı + menü/yetki/dil
satırları + route `Authority`. Yetki `Modules` değeri: Saas/Administration `Sas|Adm`, İK `Sas|Hr` satırları + route `Authority`. Yetki `Modules` değeri: Saas/Administration `Sas|Adm`, İK `Sas|Hr`

View file

@ -113,7 +113,7 @@ DevExtreme bileşenleri bu metadata'ya göre kendini kurar.
| --- | --- | | --- | --- |
| .NET | `net10.0` (SDK 10.0.300, `global.json` ile sabit) | | .NET | `net10.0` (SDK 10.0.300, `global.json` ile sabit) |
| ABP Framework | 10.0.0 (Identity, TenantManagement, PermissionManagement, AuditLogging, OpenIddict, BlobStoring, BackgroundWorkers) | | ABP Framework | 10.0.0 (Identity, TenantManagement, PermissionManagement, AuditLogging, OpenIddict, BlobStoring, BackgroundWorkers) |
| Veritabanı | SQL Server 2022 veya PostgreSQL 17 (`PlatformConsts.DefaultDatabaseProvider`) | | Veritabanı | SQL Server 2022 veya PostgreSQL 17 (`SettingsConsts.DefaultDatabaseProvider`) |
| ORM | Entity Framework Core | | ORM | Entity Framework Core |
| Kimlik | OpenIddict (password + refresh token, `/connect/token`) | | Kimlik | OpenIddict (password + refresh token, `/connect/token`) |
| Cache | Redis (opsiyonel; `Redis:IsEnabled`) | | Cache | Redis (opsiyonel; `Redis:IsEnabled`) |
@ -169,11 +169,11 @@ sozsoft-platform/
├── ui/ # React uygulaması ├── ui/ # React uygulaması
│ └── src/ │ └── src/
│ ├── routes/ # dynamicRouter, dynamicRouteLoader, route.constant │ ├── routes/ # dynamicRouter, dynamicRouteLoader, route.constant
│ ├── views/ # list, form, admin, developerKit, public, intranet, forum, report, auth, setup │ ├── views/ # list, form, admin, developerKit, public, intranet, forum, report, auth, setup, settings, menu, branch, purchasing, ai, version, shared
│ ├── components/ # ui (tasarım sistemi), template (layout), importManager, visualDesigner, componentEditor │ ├── components/ # ui (tasarım sistemi), template (layout), importManager, visualDesigner, componentEditor
│ ├── services/ # API istemcileri (her modül için ayrı servis dosyası) │ ├── services/ # API istemcileri (her modül için ayrı servis dosyası)
│ ├── store/ # easy-peasy modelleri (auth, theme, locale, abpConfig, admin) │ ├── store/ # easy-peasy modelleri (auth, theme, locale, abpConfig, admin)
│ ├── contexts/ # ComponentContext (runtime bileşenler), Dialog, Scroll │ ├── contexts/ # ComponentContext (runtime bileşen kaydı), DemoContext
│ └── utils/ # editorScriptRuntime, workflow, hooks, hoc, biçimlendirme yardımcıları │ └── utils/ # editorScriptRuntime, workflow, hooks, hoc, biçimlendirme yardımcıları
├── configs/ ├── configs/
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler │ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
@ -184,6 +184,7 @@ sozsoft-platform/
│ └── ai/ # AI entegrasyonu (n8n akış tanımları, Ollama model kurulum notları) │ └── ai/ # AI entegrasyonu (n8n akış tanımları, Ollama model kurulum notları)
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları) ├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları)
│ # lowcode.instructions.md (artefakt üretim referansı: şemalar, örnekler) │ # lowcode.instructions.md (artefakt üretim referansı: şemalar, örnekler)
│ # lowcode-reference.instructions.md (yetenek envanteri: JSON kolon sözleşmeleri, hazır ekranlar)
│ # dotnet.instructions.md (api/ için bağlayıcı .NET/ABP kod standardı) │ # dotnet.instructions.md (api/ için bağlayıcı .NET/ABP kod standardı)
│ # list.instructions.md (modül/liste ekleme prosedürü) │ # list.instructions.md (modül/liste ekleme prosedürü)
├── CLAUDE.md # Claude Code için operasyon kuralları ├── CLAUDE.md # Claude Code için operasyon kuralları
@ -368,7 +369,6 @@ okur. Kiracı tablolarının satırları, kiracı adıyla kayıtlı veri kaynağ
| --- | --- | | --- | --- |
| `VITE_API_URL` | API kök adresi. | | `VITE_API_URL` | API kök adresi. |
| `VITE_CDN_URL` | Dosya/CDN kök adresi. | | `VITE_CDN_URL` | Dosya/CDN kök adresi. |
| `VITE_REACT_APP_VERSION` | `package.json` sürümünden beslenir. |
| `VITE_AI_URL` | AI asistanının n8n webhook kökü. | | `VITE_AI_URL` | AI asistanının n8n webhook kökü. |
| `VITE_USE_POLLING` | Dosya izlemede polling (WSL/Docker senaryoları). | | `VITE_USE_POLLING` | Dosya izlemede polling (WSL/Docker senaryoları). |
| `VITE_PWA_DEV` | Geliştirmede service worker'ı açar (varsayılan kapalı). | | `VITE_PWA_DEV` | Geliştirmede service worker'ı açar (varsayılan kapalı). |
@ -1283,11 +1283,14 @@ ekranın kendi ayrıntılı açıklamasını gösterir (`DescTr`/`DescEn`, `lowc
| Doküman | Kök menü | Bölümler | | Doküman | Kök menü | Bölümler |
| --- | --- | --- | | --- | --- | --- |
| [App.Saas.md](configs/seeds/docs/modules/App.Saas.md) | Saas (`App.Saas`, host) | Tanımlamalar, Public Site Yönetimi, Kiracılar ve Şubeler, Ayar Tanımları, Diller, Veri Kaynakları, Arka Plan İşleri, Menüler, Developer Kit, Forum Yönetimi (elle yazılır; ekranlar C# seeder ile kurulur) |
| [App.Administration.md](configs/seeds/docs/modules/App.Administration.md) | Yönetim (`App.Administration`) | Tanımlamalar (Kontaklar: satış ve satınalmanın ortak müşteri/tedarikçi kartı ve sekmeleri, kontak etiketleri) | | [App.Administration.md](configs/seeds/docs/modules/App.Administration.md) | Yönetim (`App.Administration`) | Tanımlamalar (Kontaklar: satış ve satınalmanın ortak müşteri/tedarikçi kartı ve sekmeleri, kontak etiketleri) |
| [App.Hr.md](configs/seeds/docs/modules/App.Hr.md) | İnsan Kaynakları (`App.Hr`) | Tanımlamalar (dizin + genel tanımlar), Organizasyon ve Yetkinlik, Personel Kaydı (Kimlik › Kullanıcılar › detay), İşe Alım, Çalışma Takvimi ve Vardiyalar, İzin Yönetimi, Puantaj ve Devam Takibi, Sağlık Kartları, Eğitimler, Performans Yönetimi, Disiplin Dosyaları, Araçlar, Ziyaretçiler, Olay Günlüğü, Intranet | | [App.Hr.md](configs/seeds/docs/modules/App.Hr.md) | İnsan Kaynakları (`App.Hr`) | Tanımlamalar (dizin + genel tanımlar), Organizasyon ve Yetkinlik, Personel Kaydı (Kimlik › Kullanıcılar › detay), İşe Alım, Çalışma Takvimi ve Vardiyalar, İzin Yönetimi, Puantaj ve Devam Takibi, Sağlık Kartları, Eğitimler, Performans Yönetimi, Disiplin Dosyaları, Araçlar, Ziyaretçiler, Olay Günlüğü, Intranet |
| [App.Prj.md](configs/seeds/docs/modules/App.Prj.md) | Proje (`App.Prj`) | Tanımlamalar, İş Paketleri, Günce Listesi, Projeler, Adam/Ay Oranları, Kaynak Listesi, Personel Adam/Ay Oranları, Proje Bütçe Raporu |
| [App.Prc.md](configs/seeds/docs/modules/App.Prc.md) | Satınalma (`App.Prc`) | Tanımlamalar (ödeme/teslimat koşulları, ödeme/teslim yöntemleri, red nedenleri) · Talepler · Siparişler (sözleşme, tedarikçi bazlı teklif talebi ihalesi + fiyat/termin kıyası, satınalma siparişi) | | [App.Prc.md](configs/seeds/docs/modules/App.Prc.md) | Satınalma (`App.Prc`) | Tanımlamalar (ödeme/teslimat koşulları, ödeme/teslim yöntemleri, red nedenleri) · Talepler · Siparişler (sözleşme, tedarikçi bazlı teklif talebi ihalesi + fiyat/termin kıyası, satınalma siparişi) |
| [App.Inventory.md](configs/seeds/docs/modules/App.Inventory.md) | Envanter (`App.Inventory`) | Tanımlamalar (ürün kategorileri, nitelikler, ürünler ve varyantlar, reçeteler, reçete patlatma, depolar, konumlar, operasyon türleri), Operasyonlar (alım, teslimat, iç transfer; lot/seri, backorder, iade), Raporlama (stok, hareketler) | | [App.Inventory.md](configs/seeds/docs/modules/App.Inventory.md) | Envanter (`App.Inventory`) | Tanımlamalar (ürün kategorileri, nitelikler, ürünler ve varyantlar, reçeteler, reçete patlatma, depolar, konumlar, operasyon türleri), Operasyonlar (alım, teslimat, iç transfer; lot/seri, backorder, iade), Raporlama (stok, hareketler) |
| [App.Production.md](configs/seeds/docs/modules/App.Production.md) | Üretim (`App.Production`) | Tanımlamalar (iş merkezi etiketleri, iş merkezleri, operasyonlar) | | [App.Production.md](configs/seeds/docs/modules/App.Production.md) | Üretim (`App.Production`) | Tanımlamalar (iş merkezi etiketleri, iş merkezleri, operasyonlar) |
| [App.Maintenance.md](configs/seeds/docs/modules/App.Maintenance.md) | Bakım (`App.Maintenance`) | Tanımlamalar |
--- ---
@ -1354,6 +1357,10 @@ ekranın kendi ayrıntılı açıklamasını gösterir (`DescTr`/`DescEn`, `lowc
`Accept-Language`'tan kurulur. `TenantLocalizationMiddleware` + `TenantLocalizationInitializer` istek dışı `Accept-Language`'tan kurulur. `TenantLocalizationMiddleware` + `TenantLocalizationInitializer` istek dışı
iş parçacıklarının (Hangfire, worker) varsayılan kültürünü ve `IClock` tipini global `Abp.Localization.DefaultLanguage` iş parçacıklarının (Hangfire, worker) varsayılan kültürünü ve `IClock` tipini global `Abp.Localization.DefaultLanguage`
/ `Abp.Timing.TimeZone` ayarlarından tutar; değerler dakikada en fazla bir kez okunur ve yalnızca değişince yazılır. / `Abp.Timing.TimeZone` ayarlarından tutar; değerler dakikada en fazla bir kez okunur ve yalnızca değişince yazılır.
Sunucuda kullanıcıya giden tarih/sayı tek yoldan biçimlenir: `CultureFormat` (`Domain.Shared/Localization`) ile
`CultureInfo.CurrentCulture` — koda `dd.MM.yyyy` ya da `tr-TR` gibi sabit desen/kültür yazılmaz; bildirim ve worker
metinleri kiracının `DefaultLanguage` ayarıyla üretilir (`INotificationCulture`), rapor metinleri (`App.Reports.PageOf`,
Evet/Hayır) localizer'dan gelir.
--- ---
@ -1523,13 +1530,13 @@ kuyruğu arka plan işçisiyle çalıştırır ve sonuçları günlükler.
### 13.8. AI entegrasyonu ### 13.8. AI entegrasyonu
`AiBot` kayıtları tenant bazlıdır; n8n webhook üzerinden bir niyet sınıflandırma adımı ve `AiBot` kayıtları tenant bazlıdır; n8n webhook üzerinden bir niyet sınıflandırma adımı ve
ardından sohbet ya da SQL dalı çalışır (`configs/ai/Chat.json`). Model, DevOps makinesindeki ardından sohbet ya da SQL dalı çalışır (`configs/ai/chat_SQL_gemma3_4b_Select_Summary_Pivot_Grouping.json`). Model, DevOps makinesindeki
Ollama'da yerel olarak koşar (`http://ollama:11434/api/chat`, varsayılan `qwen2.5:3b`) — dışarıya Ollama'da yerel olarak koşar (`http://ollama:11434/api/chat`, varsayılan `qwen2.5:3b`) — dışarıya
model çağrısı yapılmaz. UI tarafında `/admin/ai` ve header'daki asistan bileşeni bu uç noktayı model çağrısı yapılmaz. UI tarafında `/admin/ai` ve header'daki asistan bileşeni bu uç noktayı
kullanır. kullanır.
Ollama'ya yüklü modelleri yönetmek için `configs/ai/Kurulum.txt`, sohbet arayüzü (Open WebUI) ve Ollama'ya yüklü modelleri yönetmek için `configs/ai/install_ollama.txt`, sohbet arayüzü (Open WebUI) ve
alan adı yönlendirmesi için §20 kullanılır. Akıştaki model adı değiştirilirse `configs/ai/Chat.json` alan adı yönlendirmesi için §20 kullanılır. Akıştaki model adı değiştirilirse akış dosyası
içindeki üç varsayılanın (`intentModel`, `chatModel`, `sqlModel`) birlikte güncellenmesi gerekir; içindeki üç varsayılanın (`intentModel`, `chatModel`, `sqlModel`) birlikte güncellenmesi gerekir;
biri kalırsa o dal "model not found" ile düşer. biri kalırsa o dal "model not found" ile düşer.
@ -1659,7 +1666,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| Yetki | Kim alabilir | Ne açar | Açmadığı / sınırı | | Yetki | Kim alabilir | Ne açar | Açmadığı / sınırı |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez | | `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez |
| `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `get_dev_request`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister | | `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `get_dev_request`, `get_module_doc`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister |
| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `save_sql_object`, `generate_crud_endpoints`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, `save_module_doc`, `start_dev_request`, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz | | `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `save_sql_object`, `generate_crud_endpoints`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, `save_module_doc`, `start_dev_request`, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
**Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı) **Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı)
@ -1676,6 +1683,8 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `verify_wizard` | — | — | `App.Listforms.Wizard`; seed kapanışı için `.Export` (yoksa o kontrol atlanır, doğrulama düşmez) | | `verify_wizard` | — | — | `App.Listforms.Wizard`; seed kapanışı için `.Export` (yoksa o kontrol atlanır, doğrulama düşmez) |
| `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) | | `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) |
| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete`; `dropTables` için ayrıca `App.SqlQueryManager` | | `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete`; `dropTables` için ayrıca `App.SqlQueryManager` |
| `get_module_doc` | — | — | `App.Listforms.Wizard` |
| `save_module_doc` | — | `.Write` | `App.Listforms.Wizard` + `.Update` |
| `create_table`, `save_sql_object` | — | `.Write` | `App.SqlQueryManager` | | `create_table`, `save_sql_object` | — | `.Write` | `App.SqlQueryManager` |
| `generate_crud_endpoints` | — | `.Write` | `App.SqlQueryManager.CrudEndpoints` | | `generate_crud_endpoints` | — | `.Write` | `App.SqlQueryManager.CrudEndpoints` |
| `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` | | `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` |
@ -1926,7 +1935,7 @@ konfigürasyonu ile üretilir.
- **easy-peasy** global store: `auth`, `theme`, `locale`, `abpConfig`, `admin`, `client`, `base`. - **easy-peasy** global store: `auth`, `theme`, `locale`, `abpConfig`, `admin`, `client`, `base`.
Oturum ve tercih verileri `localStorage` yerine store üzerinden yönetilir (`stateSync.ts`). Oturum ve tercih verileri `localStorage` yerine store üzerinden yönetilir (`stateSync.ts`).
- **React Query**: sunucu verisi önbellekleme. - **React Query**: sunucu verisi önbellekleme.
- **Context'ler**: `ComponentContext` (runtime bileşen kaydı), `DialogContext`, `ScrollContext`. - **Context'ler**: `ComponentContext` (runtime bileşen kaydı), `DemoContext` (demo talebi diyaloğu).
### 18.3. Tema ve lokalizasyon ### 18.3. Tema ve lokalizasyon
@ -1937,6 +1946,12 @@ konfigürasyonu ile üretilir.
ilk açılış varsayılanıdır. ilk açılış varsayılanıdır.
- Dil metinleri veritabanından gelir (`Sozsoft.Languages`); dil seçici header'dadır. - Dil metinleri veritabanından gelir (`Sozsoft.Languages`); dil seçici header'dadır.
Tenant bazlı çeviri geçersiz kılma desteklenir. Tenant bazlı çeviri geçersiz kılma desteklenir.
- Tarih ve sayı biçimi aktif dilden gelir: editör/kolon `displayFormat` değerleri DevExtreme'in adlı biçimleridir
(`shortDate`, `shortDateShortTime`, `shortTime`; `EditorOptions.Date/DateTime/Time` ↔ `presets.ts` aynası), sunucu
tarafı tek yol `CultureFormat` + `CultureInfo.CurrentCulture`'dır — sabit desen (`dd/MM/yyyy`) ne seed'e ne koda yazılır.
İstemcide kullanıcıya görünen her tarih/sayı `ui/src/utils/localeFormat.ts` üzerinden biçimlenir (`formatLocaleDate/Time/
DateTime/Number/Currency`), DevExtreme arayüz metinleri `configureDevExtreme`'de `loadMessages` ile dile göre yüklenir;
izinli makine biçimleri `ui/README.md` › "Tarih ve sayı biçimi".
- Layout seçenekleri: dikey menü, yatay menü, yığılmış yan menü, mobil navigasyon. - Layout seçenekleri: dikey menü, yatay menü, yığılmış yan menü, mobil navigasyon.
### 18.4. PWA ve sürüm yönetimi ### 18.4. PWA ve sürüm yönetimi
@ -1977,6 +1992,16 @@ konfigürasyonu ile üretilir.
- Deploy algılaması service worker'ın kendi `registration.update()` kontrolüne dayanır: - Deploy algılaması service worker'ın kendi `registration.update()` kontrolüne dayanır:
herhangi bir chunk değiştiğinde entry hash'i, dolayısıyla index.html ve `sw.js` içindeki herhangi bir chunk değiştiğinde entry hash'i, dolayısıyla index.html ve `sw.js` içindeki
precache manifest'i de değişir. precache manifest'i de değişir.
- **Deploy sonrası eksik chunk yoktur.** UI konteyneri nginx ile servis eder
(`ui/Sozsoft.Platform.Ui.Dockerfile`, `ui/nginx/default.conf`): hash'li `assets/` dosyaları
`ui-assets` volume'unda biriktirilir (`ui/nginx/40-archive-assets.sh` açılışta yeni sürümün
dosyalarını ekler, 30 günden eski ve artık hiçbir sürümde olmayanları siler), böylece
deploy'dan önce açılmış sekmeler kendi sürümlerinin chunk'larını bulmaya devam eder.
`index.html`/`sw.js`/`warmup.json`/`manifest.webmanifest` `no-store`, hash'li dosyalar
`immutable` başlığıyla döner. Yine de bir chunk bulunamazsa (`vite:preloadError`) uygulama
kabuk cache'ini silip sayfayı bir kez yeniler; ikinci başarısızlıkta hata sınırı
"uygulama güncellendi, yenileyin" mesajı ve Yenile butonu gösterir
(`swRegistration.isChunkLoadError` / `reloadApp`). Ayrıntı: `ui/README.md`.
--- ---

View file

@ -6,4 +6,15 @@
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<!--
ABP paketleri System.Security.Cryptography.Pkcs 10.0.0'ı geçişli olarak getiriyor (GHSA-846f-f764-76mx,
NU1903). Paket çerçeveden budanmadığı için yamalı sürüm bütün projelere tek yerden verilir; ABP yamalı
sürümü kendisi çekince satır kaldırılır (dotnet list package - -vulnerable - -include-transitive).
Projelerin kendi doğrudan referansı olan paketler (MessagePack, Cryptography.Xml) kendi csproj'larında güncel tutulur.
-->
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
</ItemGroup>
</Project> </Project>

View file

@ -55,11 +55,11 @@ username=system%40sozsoft.com
- WhatsApp -> API Setup kısmından "From" kısmında yeni telefon numarası eklenir. - WhatsApp -> API Setup kısmından "From" kısmında yeni telefon numarası eklenir.
- WhatsApp -> API Setup kısmından Generate Access Token oluşturulur. - WhatsApp -> API Setup kısmından Generate Access Token oluşturulur.
- token: EAASM83pDJf0BO0ZCYBr3Fx4SES0ox8XiZCgP3FIteprYpwawZCkGYjpMlKk4OZCWgRNZC2Ttofgf8amMf929ZBuDcgYIOsXkPMGBNmVxm2czipzq63LmtbYfyCWsKVy0q1jtay0nRoeDuO2FKhqCfLLtNn9cSWjINVLcz26ptK8a2Oko83cZBzrNsp3cpIECnzqRfEjHdEz1U73y3ZB1MZBOhHilax7yZALf4ZA1Tmk - token: <access token>
- WhatsApp telefon numarasını Register etmek için aşağıdaki postman Curl gönderilir. - WhatsApp telefon numarasını Register etmek için aşağıdaki postman Curl gönderilir.
``` ```
curl --location 'https://graph.facebook.com/v21.0/521106361082067/register' \ curl --location 'https://graph.facebook.com/v21.0/<phone-number-id>/register' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access token>' \ --header 'Authorization: Bearer <access token>' \
--data '{ --data '{
@ -68,7 +68,7 @@ username=system%40sozsoft.com
}' }'
``` ```
- Message göndermek için url : https://graph.facebook.com/v21.0/521106361082067/messages - Message göndermek için url : https://graph.facebook.com/v21.0/<phone-number-id>/messages
``` ```
{ {

View file

@ -11,14 +11,6 @@ public partial class LanguageToLanguageInfoMapper : MapperBase<Language, Languag
public override partial LanguageInfo Map(Language source); public override partial LanguageInfo Map(Language source);
public override partial void Map(Language source, LanguageInfo destination); public override partial void Map(Language source, LanguageInfo destination);
public override void BeforeMap(Language source)
{
}
public override void AfterMap(Language source, LanguageInfo destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -27,14 +19,6 @@ public partial class LanguageToLanguageDtoMapper : MapperBase<Language, Language
public override partial LanguageDto Map(Language source); public override partial LanguageDto Map(Language source);
public override partial void Map(Language source, LanguageDto destination); public override partial void Map(Language source, LanguageDto destination);
public override void BeforeMap(Language source)
{
}
public override void AfterMap(Language source, LanguageDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -43,14 +27,6 @@ public partial class LanguageKeyToLanguageKeyDtoMapper : MapperBase<LanguageKey,
public override partial LanguageKeyDto Map(LanguageKey source); public override partial LanguageKeyDto Map(LanguageKey source);
public override partial void Map(LanguageKey source, LanguageKeyDto destination); public override partial void Map(LanguageKey source, LanguageKeyDto destination);
public override void BeforeMap(LanguageKey source)
{
}
public override void AfterMap(LanguageKey source, LanguageKeyDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -59,14 +35,6 @@ public partial class LanguageTextToLanguageTextDtoMapper : MapperBase<LanguageTe
public override partial LanguageTextDto Map(LanguageText source); public override partial LanguageTextDto Map(LanguageText source);
public override partial void Map(LanguageText source, LanguageTextDto destination); public override partial void Map(LanguageText source, LanguageTextDto destination);
public override void BeforeMap(LanguageText source)
{
}
public override void AfterMap(LanguageText source, LanguageTextDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -75,14 +43,6 @@ public partial class LanguageTextToLanguageTextEtoMapper : MapperBase<LanguageTe
public override partial LanguageTextEto Map(LanguageText source); public override partial LanguageTextEto Map(LanguageText source);
public override partial void Map(LanguageText source, LanguageTextEto destination); public override partial void Map(LanguageText source, LanguageTextEto destination);
public override void BeforeMap(LanguageText source)
{
}
public override void AfterMap(LanguageText source, LanguageTextEto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -93,14 +53,6 @@ public partial class LanguageTextCreateUpdateDtoToLanguageTextMapper : MapperBas
[MapperIgnoreTarget(nameof(LanguageText.LanguageKey))] [MapperIgnoreTarget(nameof(LanguageText.LanguageKey))]
public override partial void Map(LanguageTextCreateUpdateDto source, LanguageText destination); public override partial void Map(LanguageTextCreateUpdateDto source, LanguageText destination);
public override void BeforeMap(LanguageTextCreateUpdateDto source)
{
}
public override void AfterMap(LanguageTextCreateUpdateDto source, LanguageText destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -133,12 +85,4 @@ public partial class LanguageKeyToLanguageTextTranslatedDtoMapper : MapperBase<L
[MapperIgnoreTarget(nameof(LanguageTextTranslatedDto.CreationTime))] [MapperIgnoreTarget(nameof(LanguageTextTranslatedDto.CreationTime))]
[MapperIgnoreTarget(nameof(LanguageTextTranslatedDto.CreatorId))] [MapperIgnoreTarget(nameof(LanguageTextTranslatedDto.CreatorId))]
public override partial void Map(LanguageKey source, LanguageTextTranslatedDto destination); public override partial void Map(LanguageKey source, LanguageTextTranslatedDto destination);
public override void BeforeMap(LanguageKey source)
{
}
public override void AfterMap(LanguageKey source, LanguageTextTranslatedDto destination)
{
}
} }

View file

@ -7,8 +7,8 @@ public abstract class MailQueueAppService : ApplicationService
{ {
protected MailQueueAppService() protected MailQueueAppService()
{ {
LocalizationResource = typeof(ErpMailQueueResource); LocalizationResource = typeof(SozsoftMailQueueResource);
ObjectMapperContext = typeof(ErpMailQueueModule); ObjectMapperContext = typeof(SozsoftMailQueueModule);
} }
} }

View file

@ -1 +0,0 @@
namespace Sozsoft.MailQueue.Application;

View file

@ -3,7 +3,7 @@
namespace Sozsoft.MailQueue.Domain.Shared; namespace Sozsoft.MailQueue.Domain.Shared;
[LocalizationResourceName(MailQueueConsts.AppName)] [LocalizationResourceName(MailQueueConsts.AppName)]
public class ErpMailQueueResource public class SozsoftMailQueueResource
{ {
} }

View file

@ -1,64 +0,0 @@
using Sozsoft.MailQueue.MailGeneration.Models;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.MailQueue.Domain.MailGeneration;
public interface IAttachmentExtractor
{
IList<AttachmentModel> Extract(string attachment, string attachmentParametre);
}
public class AttachmentExtractor : IAttachmentExtractor, ITransientDependency
{
protected readonly IParameterExtractor ParameterExtractor;
public AttachmentExtractor(
IParameterExtractor parameterExtractor)
{
ParameterExtractor = parameterExtractor;
}
public IList<AttachmentModel> Extract(string attachment, string attachmentParametre)
{
var result = new List<AttachmentModel>();
if (attachment.IsNullOrWhiteSpace())
return result;
var parameters = ParameterExtractor.Extract(attachmentParametre);
/*
Dict<
0, Dict<
Tarih, 1232
No 4334
>
1, Dict<
QUERY, ghghg
QUERYSQL select ffddf
*/
//Attachment:
//{0:MRP:rpt_DepoCikisCekiListesi:PDF:MailEkteGozukecekAd};{1:CRM:rpt_DepoCikisCekiListesiCarpet:XLS:MailEkteGozukecekAd}
//AttachmentParametre:
//Filtre değerleri olabilir {0:Tarih=1232&No=4334};{1:Tarih=213}
//Veya query olabilir {0:QUERY=…&QUERYSQL=..};{1:QUERY=…&QUERYSQL=..}
foreach (var item in attachment.Split("};{"))
{
var keyValue = item.Trim('{').Trim('}').Split(":");
if (keyValue.Length == 5)
{
result.Add(new AttachmentModel
{
Index = keyValue[0],
DbAdi = keyValue[1],
RaporAdi = keyValue[2],
ExportDosyaTipi = keyValue[3],
Filtreler = parameters.GetOrDefault(keyValue[0]),
MailEkAdi = keyValue[4]
});
}
}
return result;
}
}

View file

@ -2,85 +2,36 @@
using Sozsoft.MailQueue.FileGeneration; using Sozsoft.MailQueue.FileGeneration;
using Sozsoft.MailQueue.MailGeneration.Models; using Sozsoft.MailQueue.MailGeneration.Models;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.TextTemplating;
namespace Sozsoft.MailQueue.Domain.MailGeneration; namespace Sozsoft.MailQueue.Domain.MailGeneration;
public interface IAttachmentGenerator public interface IAttachmentGenerator
{ {
Task<Dictionary<string, string>> GenerateAsync(
Guid kuyrukId,
string attachment,
string attachmentParametre,
string jobId = null);
Task<Dictionary<string, string>> GenerateAsync(List<MailTemplateTableModel> tables, Guid? kuyrukId = null, string jobId = null); Task<Dictionary<string, string>> GenerateAsync(List<MailTemplateTableModel> tables, Guid? kuyrukId = null, string jobId = null);
} }
/// <summary>
/// Kuyruk satırındaki tablo şablonlarından (<c>DosyaTipi</c> PDF/XLS/TXT) mail eki üretir.
/// Ek üretiminin tek yolu budur; eski <c>Attachment</c>/<c>AttachmentParameter</c> kolonlarıyla
/// MS Access raporundan ek üretme yolu hiç gerçeklenmediği için kaldırıldı — kolon dolu olsa da
/// satır artık atlanmaz, tablo ekleriyle gönderilir.
/// </summary>
public class AttachmentGenerator : IAttachmentGenerator, ITransientDependency public class AttachmentGenerator : IAttachmentGenerator, ITransientDependency
{ {
public IParameterExtractor ParameterExtractor { get; }
public IAttachmentExtractor AttachmentExtractor { get; }
public ITemplateRenderer TemplateRenderer { get; }
public IGuidGenerator GuidGenerator { get; }
public IServiceProvider ServiceProvider { get; } public IServiceProvider ServiceProvider { get; }
protected ILogManager LogManager { get; } protected ILogManager LogManager { get; }
private readonly IConfiguration Configuration; private readonly IConfiguration Configuration;
public AttachmentGenerator( public AttachmentGenerator(
IAttachmentExtractor attachmentExtractor,
ITemplateRenderer templateRenderer,
IGuidGenerator guidGenerator,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogManager logManager, ILogManager logManager,
IConfiguration configuration IConfiguration configuration)
)
{ {
AttachmentExtractor = attachmentExtractor;
TemplateRenderer = templateRenderer;
GuidGenerator = guidGenerator;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
LogManager = logManager; LogManager = logManager;
Configuration = configuration; Configuration = configuration;
} }
/// <summary>
/// Verilen parametreler (attachment parametreleri) ile KIM raporlari ile istenilen formatta dosya uretir
/// </summary>
/// <param name="attachment">Export edilecek olan attachmentlar</param>
/// <param name="attachmentParametre">Her attachment için parametreler</param>
/// <returns>Uretilen ekleri Key: MailEkAdi, Value: Base64 encode edilmis dosya olan dictionary olarak döner</returns>
public async Task<Dictionary<string, string>> GenerateAsync(
Guid kuyrukId,
string attachment,
string attachmentParametre,
string jobId)
{
var result = new Dictionary<string, string>();
var models = AttachmentExtractor.Extract(attachment, attachmentParametre);
var attachmentFolder = $"{kuyrukId}-{jobId}";
foreach (var item in models)
{
var file = GenerateAttachment(item, attachmentFolder);
if (file.IsNullOrEmpty())
{
LogManager.LogError($"Attachment oluşturulamadı. Index: {item.Index}", "ATTACHMENT", "KUYRUK", kuyrukId, jobId);
continue;
}
var base64 = await ConvertFileToBase64Async(file);
var key = item.MailEkAdi;
if (result.ContainsKey(key))
{
key = result.Count + "-" + item.MailEkAdi;
}
result.Add(key, base64);
}
return result;
}
/// <summary> /// <summary>
/// Verilan tablo modeli ile istenilen dosya tipinde (PDF, XLS, TXT) ek üretir /// Verilan tablo modeli ile istenilen dosya tipinde (PDF, XLS, TXT) ek üretir
/// </summary> /// </summary>
@ -132,70 +83,6 @@ public class AttachmentGenerator : IAttachmentGenerator, ITransientDependency
return attachments; return attachments;
} }
private string GenerateAttachment(AttachmentModel model, string attachmentFolder)
{
if (model.Filtreler.IsNullOrEmpty() || model.RaporAdi.IsNullOrWhiteSpace())
{
return null;
}
return null;
// if (model.Filtreler.ContainsKey("QUERY") && model.Filtreler.ContainsKey("QUERYSQL"))
// {
// /*
//Parametrelerden asagidaki sekilde gelen queryleri en alttaki formata donusturuyoruz
//params
//0 =>
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
// QUERYSQL -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
// QUERY2SQL -> KSp_Depo_Cikis_CekiListesi_Sub 252993
//SQL olan keyvalue'lari cikar ve sadece querylerde foreach yap
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
// dict.add(item.Value, params[item.Key + "SQL"].Value)
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
// dict.add(item.Value, params[item.Key + "SQL"].Value)
//Bunlari yeni dict'e ekle
//dict
//qryDepo_Cikis_CekiListesi_Detayli -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
//qryDepo_Cikis_CekiListesi_Sub -> KSp_Depo_Cikis_CekiListesi_Sub 252993
// */
// //dict -> key (name), value (sql)
// var queries = model.Filtreler
// .Where(a => a.Key.StartsWith("QUERY"))
// .Where(a => !a.Key.EndsWith("SQL"))
// .Where(a => !a.Value.IsNullOrWhiteSpace())
// .ToDictionary(x => x.Value, x => model.Filtreler.GetValueOrDefault(x.Key + "SQL"));
// return MsAccessReportManager.GenerateAccessReport(
// model.DbAdi,
// model.Index,
// model.RaporAdi,
// model.ExportDosyaTipi,
// queries,
// attachmentFolder);
// }
// else
// {
// return MsAccessReportManager.GenerateAccessReport(
// model.DbAdi,
// model.Index,
// model.RaporAdi,
// string.Join(" AND ", model.Filtreler.Select(a => $"{a.Key}={a.Value}")),
// model.ExportDosyaTipi,
// attachmentFolder);
// }
}
private async Task<string> ConvertFileToBase64Async(string filePath)
{
var bytes = await File.ReadAllBytesAsync(filePath);
return Convert.ToBase64String(bytes);
}
private void LogToFile(Guid id, byte[] content, string fileName) private void LogToFile(Guid id, byte[] content, string fileName)
{ {
if (!Configuration.GetValue<bool>("App:LogMailToFile")) if (!Configuration.GetValue<bool>("App:LogMailToFile"))
@ -224,4 +111,3 @@ public class AttachmentGenerator : IAttachmentGenerator, ITransientDependency
} }
} }
} }

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Sozsoft.MailQueue.MailGeneration.Models; using Sozsoft.MailQueue.MailGeneration.Models;
using Volo.Abp.Timing;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.TextTemplating; using Volo.Abp.TextTemplating;
@ -23,17 +24,20 @@ public class MailBodyGenerator : IMailBodyGenerator, ITransientDependency
public IParameterExtractor ParameterExtractor { get; } public IParameterExtractor ParameterExtractor { get; }
public ITemplateRenderer TemplateRenderer { get; } public ITemplateRenderer TemplateRenderer { get; }
protected ILogManager LogManager { get; } protected ILogManager LogManager { get; }
protected IClock Clock { get; }
public MailBodyGenerator( public MailBodyGenerator(
IParameterExtractor parameterExtractor, IParameterExtractor parameterExtractor,
ITemplateRenderer templateRenderer, ITemplateRenderer templateRenderer,
IConfiguration configuration, IConfiguration configuration,
ILogManager logManager) ILogManager logManager,
IClock clock)
{ {
ParameterExtractor = parameterExtractor; ParameterExtractor = parameterExtractor;
TemplateRenderer = templateRenderer; TemplateRenderer = templateRenderer;
this.configuration = configuration; this.configuration = configuration;
LogManager = logManager; LogManager = logManager;
Clock = clock;
} }
/// <summary> /// <summary>
@ -84,7 +88,7 @@ public class MailBodyGenerator : IMailBodyGenerator, ITransientDependency
return; return;
} }
path = Path.Combine(path, id.ToString()); path = Path.Combine(path, id.ToString());
var date = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"); var date = Clock.Now.ToString("yyyy-MM-dd-HH-mm-ss");
var dosyaAdi = $"mail-{id}-{date}.html "; var dosyaAdi = $"mail-{id}-{date}.html ";
try try
{ {

View file

@ -1,12 +0,0 @@
namespace Sozsoft.MailQueue.MailGeneration.Models;
public class AttachmentModel
{
public string Index { get; set; }
public string DbAdi { get; set; } //MRP.mdb, CRM.mdb
public string RaporAdi { get; set; }
public string ExportDosyaTipi { get; set; } //PDF, TXT, XLS, ..
public Dictionary<string, string> Filtreler { get; set; }
public string MailEkAdi { get; set; }
}

View file

@ -1,4 +1,6 @@
namespace Sozsoft.MailQueue.MailGeneration.Models; using Sozsoft.Platform.Localization;
namespace Sozsoft.MailQueue.MailGeneration.Models;
public class MailTemplateTableColumnModel public class MailTemplateTableColumnModel
{ {
@ -15,5 +17,10 @@ public class MailTemplateTableColumnModel
public string AltToplam { get; set; } public string AltToplam { get; set; }
public int Genislik { get; set; } public int Genislik { get; set; }
/// <summary>
/// Hucre metni: <c>DataFormat</c> deseni aktif kulturde uygulanir (HTML/PDF/TXT ortak yol).
/// XLS bu yoldan gecmez; orada desen Excel bicim kodu olarak hucreye yazilir.
/// </summary>
public string FormatValue(object value) => CultureFormat.Value(value, VeriFormati);
} }

View file

@ -1,11 +1,9 @@
using Microsoft.Extensions.Configuration; using Sozsoft.MailQueue.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
using Sozsoft.MailQueue.Domain.MailGeneration; using Sozsoft.MailQueue.Domain.MailGeneration;
using Sozsoft.MailQueue.Domain.Shared; using Sozsoft.MailQueue.Domain.Shared;
using Sozsoft.Sender.Mail; using Sozsoft.Sender.Mail;
using Volo.Abp.BackgroundWorkers; using Volo.Abp.BackgroundWorkers;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.Timing; using Volo.Abp.Timing;
using Volo.Abp.Uow; using Volo.Abp.Uow;
@ -18,106 +16,64 @@ public class MailQueueWorker : BackgroundWorkerBase
public MailQueueWorkerOptions Options { get; set; } public MailQueueWorkerOptions Options { get; set; }
protected ISozsoftEmailSender ErpEmailSender { get; } protected ISozsoftEmailSender ErpEmailSender { get; }
protected IRepository<Entities.BackgroundWorker_MailQueue> Repository { get; } protected IRepository<BackgroundWorker_MailQueue> Repository { get; }
public IClock Clock { get; } public IClock Clock { get; }
public IConfiguration Configuration { get; }
public ILogManager LogManager { get; } public ILogManager LogManager { get; }
public ITableGenerator TableGenerator { get; } public ITableGenerator TableGenerator { get; }
public IMailBodyGenerator MailBodyGenerator { get; } public IMailBodyGenerator MailBodyGenerator { get; }
public IAttachmentGenerator AttachmentGenerator { get; } public IAttachmentGenerator AttachmentGenerator { get; }
public IGuidGenerator GuidGenerator { get; }
public MailQueueWorker( public MailQueueWorker(
ISozsoftEmailSender erpEmailSender, ISozsoftEmailSender erpEmailSender,
IRepository<Entities.BackgroundWorker_MailQueue> repository, IRepository<BackgroundWorker_MailQueue> repository,
IClock clock, IClock clock,
IConfiguration configuration,
ILogManager logManager, ILogManager logManager,
ITableGenerator tableGenerator, ITableGenerator tableGenerator,
IMailBodyGenerator mailBodyGenerator, IMailBodyGenerator mailBodyGenerator,
IAttachmentGenerator attachmentGenerator, IAttachmentGenerator attachmentGenerator)
IGuidGenerator guidGenerator
)
{ {
ErpEmailSender = erpEmailSender; ErpEmailSender = erpEmailSender;
Repository = repository; Repository = repository;
Clock = clock; Clock = clock;
Configuration = configuration;
LogManager = logManager; LogManager = logManager;
TableGenerator = tableGenerator; TableGenerator = tableGenerator;
MailBodyGenerator = mailBodyGenerator; MailBodyGenerator = mailBodyGenerator;
AttachmentGenerator = attachmentGenerator; AttachmentGenerator = attachmentGenerator;
GuidGenerator = guidGenerator;
} }
/// <summary> /// <summary>
/// ID'si verilen Template satırının kuyruk kayıtlarını proses ederek bildirimleri yapar /// Şablonun (TemplateId) gönderilmemiş kuyruk kayıtlarını işler: her kayıt için tablo, gövde ve
/// ekler üretilir, mail gönderilir, sonuç kayda yazılır. Akış: tablo → gövde → ekler → gönderim.
/// </summary> /// </summary>
public override async Task StartAsync(CancellationToken cancellationToken = default) public override async Task StartAsync(CancellationToken cancellationToken = default)
{ {
/*
* TemplateId'den ilgili template satırını al
* TemplateId'den ilgili kuyruk kayıtlarını al
* Bu kurallar inclusive'dir.
* Yani birden fazla kural aynı kuyruk kaydı için işletilebilir.
* BeforeSP varsa çalıştır.
* SP görevini yaptıktan sonra devam et
* Template:
* Dosya Tipi: NULL, PDF, XLS, IN
* NULL: Sadece mail body
* PDF, XLS, TXT, XML, HTML, RTF: Bu durumda ilgili rapor hazırlanıp eklenir
* IN: Tablo, mail body'e gömülür
* Tablo:
* Mail Body metni oluşturulacak
* Kuyruk tablosu MAILPARAMETRE alanı ile
* MAILTEMPLATE'de belirtilen HTML üretilerek
* mail body oluşturulur
* Tablo Alanı dolu ise;
* Belirtilen tablodan kayıtları al ve Kuyruk'taki
* TABLOPARAMETRE ve TABLO alanlarına göre tabloyu oluştur
* ve Dosya Tipi'ne göre bildirime ekle.
* Bu durumda ilgili Tabloda belirtilen css ve başlıklar kullanılmalı
* Kuyruk - ATTACHMENT ve ATTACHMENTPARAMETRE dolu ise;
* Bu alanlarda belirtilen raporlar Access API ile
* oluşturulup DOSYA TIPI'ne göre bildirime eklenecek
* Gönderimi Yap
* KUYRUK - GATEWAYTEMPLATEID alanında belirtilen template ile
* mail gateway'de ilgili template gönderimi yapılır
* KIMDEN ve KIME alanları kullanılır.
* Gateway'e gönderim emri verildikten sonra
* GonderilmeTarihi alanı setlenir.
* AFTERSP'de belirtilen SP'yi veritabanında çalıştır
*
*/
/*
Options:
- Mail gidecek kişiler listesi
- Mail subject
- Mail lojik türü
- Kayıt kaynağı
- RS Filtre (id, vs)
-
1- Template Id'ye göre, Kuyruk tablosunu filtrele
2- Her kayıt için mail gönder
*/
using var scope = ServiceProvider.CreateScope();
var uowManager = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>(); var uowManager = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>();
using var uow = uowManager.Begin(); using var uow = uowManager.Begin();
var jobId = $"{TemplateId}-{Clock.Now:yyyy-MM-dd-HH-mm-ss}"; var jobId = $"{TemplateId}-{Clock.Now:yyyy-MM-dd-HH-mm-ss}";
var attachmentsBasePath = Configuration.GetValue<string>("App:AttachmentsPath");
//var template = await RepositoryTemplate.GetAsync(a => a.Id == templateId);
var queues = await Repository.GetListAsync(a => a.TemplateId == TemplateId && !a.SendStatus, cancellationToken: cancellationToken); var queues = await Repository.GetListAsync(a => a.TemplateId == TemplateId && !a.SendStatus, cancellationToken: cancellationToken);
foreach (var queue in queues) foreach (var queue in queues)
{ {
// Her kayıt kendi UoW'sinde: bir kaydın hatası diğerlerinin gönderimini ve kaydını düşürmez.
using var uow2 = uowManager.Begin(requiresNew: true, isTransactional: false); using var uow2 = uowManager.Begin(requiresNew: true, isTransactional: false);
try try
{ {
// Tablo oluştur await ProcessQueueAsync(queue, jobId, cancellationToken);
await uow2.CompleteAsync(cancellationToken);
}
catch (Exception ex)
{
LogManager.LogError($"Hata. Kuyruk Id: {queue.Id}. {ex}", "HATA", "KUYRUK", queue.Id, jobId);
}
}
await uow.CompleteAsync(cancellationToken);
}
private async Task ProcessQueueAsync(BackgroundWorker_MailQueue queue, string jobId, CancellationToken cancellationToken)
{
var tables = await TableGenerator.GenerateAsync(queue.TableName, queue.TableParameter, queue.Id); var tables = await TableGenerator.GenerateAsync(queue.TableName, queue.TableParameter, queue.Id);
// Mail Body oluştur
var mailBody = await MailBodyGenerator.GenerateAsync( var mailBody = await MailBodyGenerator.GenerateAsync(
TemplateName, TemplateName,
queue.MailParameter, queue.MailParameter,
@ -125,39 +81,15 @@ public class MailQueueWorker : BackgroundWorkerBase
tables, tables,
jobId); jobId);
// Attachment oluştur // Ekler tablo şablonlarından (IN olmayanlar: PDF, XLS, TXT) üretilir.
// Iki attachment türü var: var attachments = await AttachmentGenerator.GenerateAsync(tables.Values.ToList(), queue.Id, jobId);
// 1- ATTACHMENT alanı dolu olanlar // IN olmayan bir tablo belirtilmiş ama hiçbiri üretilememişse hata vardır, gönderilmez.
var attachments = await AttachmentGenerator.GenerateAsync(queue.Id, queue.Attachment, queue.AttachmentParameter, jobId); if (tables.Values.Any(a => a.DosyaTipi != "IN") && attachments.IsNullOrEmpty())
if (!queue.Attachment.IsNullOrWhiteSpace() && attachments.IsNullOrEmpty())
{ {
//Kuyruk uzerinde Attachment var fakat hicbiri olusturulamis, devam etmeyelim
LogManager.LogWarning("Attachmentlar oluşturulamadı", "ATTACHMENT", "KUYRUK", queue.Id, jobId);
continue;
}
// 2- Tablo (IN olmayanlar-PDF,XLS,TXT)
var attachmentsTable = await AttachmentGenerator.GenerateAsync(tables.Values.ToList(), queue.Id, jobId);
// IN olmayan herhangi bir tablo belirtilmisse fakat bu tablo attachment olarak uretilememisse, hata vardir
if (tables.Values.Where(a => a.DosyaTipi != "IN").Any() && attachmentsTable.IsNullOrEmpty())
{
//NULL ise hata olmustur, devam etmeyelim
LogManager.LogWarning("Tablo Attachmentlar oluşturulamadı", "ATTACHMENT", "KUYRUK", queue.Id, jobId); LogManager.LogWarning("Tablo Attachmentlar oluşturulamadı", "ATTACHMENT", "KUYRUK", queue.Id, jobId);
continue; return;
} }
foreach (var attachmentTable in attachmentsTable)
{
var key = attachmentTable.Key;
if (attachments.ContainsKey(key))
{
key += "-" + GuidGenerator.Create();
}
attachments.Add(key, attachmentTable.Value);
}
// Gönderimi yap
var senderKeyValue = queue.From.Split(":"); var senderKeyValue = queue.From.Split(":");
var sender = new KeyValuePair<string, string>( var sender = new KeyValuePair<string, string>(
senderKeyValue.ElementAtOrDefault(0), senderKeyValue.ElementAtOrDefault(0),
@ -180,33 +112,9 @@ public class MailQueueWorker : BackgroundWorkerBase
LogManager.LogWarning($"Mail gönderilemedi. Gelen Mesaj: {result.ErrorMessage}", "MAILSEND", "KUYRUK", queue.Id, jobId); LogManager.LogWarning($"Mail gönderilemedi. Gelen Mesaj: {result.ErrorMessage}", "MAILSEND", "KUYRUK", queue.Id, jobId);
queue.AwsMessageId = result.ErrorMessage; queue.AwsMessageId = result.ErrorMessage;
} }
queue.SendStatus = result.Success; queue.SendStatus = result.Success;
queue.SendTime = Clock.Now; queue.SendTime = Clock.Now;
await Repository.UpdateAsync(queue, cancellationToken: cancellationToken); await Repository.UpdateAsync(queue, cancellationToken: cancellationToken);
// Attachmentlari silebiliriz
try
{
var attachmentsPath = Path.Combine(attachmentsBasePath, $"{queue.Id}-{jobId}");
if (Directory.Exists(attachmentsPath))
{
Directory.Delete(attachmentsPath, true);
} }
} }
catch (Exception ex)
{
LogManager.LogError($"Attachments klasörü silinemedi. Kuyruk Id: {queue.Id}. {ex}", "HATA", "ATTACHMENT", queue.Id, jobId);
}
}
catch (Exception ex)
{
LogManager.LogError($"Hata. Kuyruk Id: {queue.Id}. {ex}", "HATA", "KUYRUK", queue.Id, jobId);
}
await uow2.SaveChangesAsync(cancellationToken);
}
await uow.SaveChangesAsync(cancellationToken);
}
}

View file

@ -1,6 +1,7 @@
using System.Data; using System.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Sozsoft.MailQueue.Domain.Entities; using Sozsoft.MailQueue.Domain.Entities;
using Sozsoft.Platform.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -20,34 +21,50 @@ public class QueryRepository : IQueryRepository, ITransientDependency
Repository = repository; Repository = repository;
} }
/// <summary>
/// Mail şablonunun tablo kaynağını okur. Tablo ve kolon adları şablon kaydından gelir, parametre
/// olarak bağlanamaz; bu yüzden beyaz listeden geçirilip ANSI tırnakla yazılır (iki sağlayıcıda da
/// geçerli), değerler ise parametredir.
/// </summary>
public async Task<DataTable> QueryAsync(string tabloAdi, Dictionary<string, string> where) public async Task<DataTable> QueryAsync(string tabloAdi, Dictionary<string, string> where)
{ {
if (!SqlIdentifier.IsSafe(tabloAdi))
throw new ArgumentException($"Invalid table name: {tabloAdi}", nameof(tabloAdi));
var table = new DataTable(); var table = new DataTable();
var ctx = await Repository.GetDbContextAsync(); var ctx = await Repository.GetDbContextAsync();
var cmd = ctx.Database.GetDbConnection().CreateCommand(); var connection = ctx.Database.GetDbConnection();
var query = $"SELECT * FROM {tabloAdi} WHERE "; await using var cmd = connection.CreateCommand();
var wheres = new List<string>(); var wheres = new List<string>();
var index = 0;
foreach (var item in where) foreach (var item in where)
{ {
var parameterName = $"@p{item.Key}"; if (!SqlIdentifier.IsSafe(item.Key))
wheres.Add($"{item.Key}={parameterName}"); throw new ArgumentException($"Invalid column name: {item.Key}", nameof(where));
var parameterName = $"@p{index++}";
wheres.Add($"{SqlIdentifier.QuoteAnsi(item.Key)} = {parameterName}");
var prm = cmd.CreateParameter(); var prm = cmd.CreateParameter();
prm.ParameterName = parameterName; prm.ParameterName = parameterName;
prm.Value = item.Value; prm.Value = item.Value;
cmd.Parameters.Add(prm); cmd.Parameters.Add(prm);
} }
query += string.Join(" AND ", wheres);
cmd.CommandText = query; cmd.CommandText = $"SELECT * FROM {SqlIdentifier.QuoteAnsi(tabloAdi)}"
+ (wheres.Count > 0 ? " WHERE " + string.Join(" AND ", wheres) : string.Empty);
await connection.OpenAsync();
try try
{ {
cmd.Connection.Open(); await using var reader = await cmd.ExecuteReaderAsync();
table.Load(await cmd.ExecuteReaderAsync()); table.Load(reader);
} }
finally finally
{ {
cmd.Connection.Close(); await connection.CloseAsync();
} }
return table; return table;
} }
} }

View file

@ -16,7 +16,7 @@ public class TXTFileGenerator : IFileGenerator, ITransientDependency
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
table.TabloDegerleri.ForEach(row => table.TabloDegerleri.ForEach(row =>
sb.AppendLine(string.Join('\t', table.TabloSutunlari.Select(col => row.GetValueOrDefault(col.SutunAdi))))); sb.AppendLine(string.Join('\t', table.TabloSutunlari.Select(col => col.FormatValue(row.GetValueOrDefault(col.SutunAdi))))));
return await Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString())); return await Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString()));
} }

View file

@ -119,7 +119,7 @@ table.tablo tbody tr:hover {
{ {
<tr> <tr>
@foreach (var sutun in tablo0.TabloSutunlari){ @foreach (var sutun in tablo0.TabloSutunlari){
<td>@satir.GetValueOrDefault(sutun.SutunAdi)</td> <td>@sutun.FormatValue(satir.GetValueOrDefault(sutun.SutunAdi))</td>
} }
</tr> </tr>
} }

View file

@ -1,4 +1,7 @@
using ClosedXML.Excel; using System.Globalization;
using ClosedXML.Excel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Sozsoft.MailQueue.MailGeneration.Models; using Sozsoft.MailQueue.MailGeneration.Models;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -6,6 +9,8 @@ namespace Sozsoft.MailQueue.FileGeneration;
public class XLSFileGenerator : IFileGenerator, ITransientDependency public class XLSFileGenerator : IFileGenerator, ITransientDependency
{ {
public ILogger<XLSFileGenerator> Logger { get; set; } = NullLogger<XLSFileGenerator>.Instance;
string sheetPassword = "Erp123"; string sheetPassword = "Erp123";
public async Task<byte[]> Generate(MailTemplateTableModel table) public async Task<byte[]> Generate(MailTemplateTableModel table)
@ -87,7 +92,8 @@ public class XLSFileGenerator : IFileGenerator, ITransientDependency
switch (dataType) switch (dataType)
{ {
case XLDataType.Number: case XLDataType.Number:
cell.SetValue(Convert.ToInt32(value)); // Int32'ye cevirmek ondaligi atardi; hucre sayisal kalir, gosterimi Excel bicim kodu verir.
cell.SetValue(Convert.ToDecimal(value, CultureInfo.InvariantCulture));
break; break;
case XLDataType.DateTime: case XLDataType.DateTime:
cell.SetValue(Convert.ToDateTime(value)); cell.SetValue(Convert.ToDateTime(value));
@ -166,7 +172,11 @@ public class XLSFileGenerator : IFileGenerator, ITransientDependency
cell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin); cell.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thin);
} }
} }
catch { } catch (Exception ex)
{
// Geçersiz CSS değeri hücreyi biçimsiz bırakır; dosya üretimi sürer.
Logger.LogWarning(ex, "Hücre CSS'i uygulanamadı: {Css}", string.Join(";", css.Select(a => a.Key + ":" + a.Value)));
}
} }
} }

View file

@ -42,6 +42,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Sozsoft.Sender\Sozsoft.Sender.csproj" /> <ProjectReference Include="..\Sozsoft.Sender\Sozsoft.Sender.csproj" />
<ProjectReference Include="..\..\src\Sozsoft.Platform.Domain.Shared\Sozsoft.Platform.Domain.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -26,22 +26,22 @@ namespace Sozsoft.MailQueue;
typeof(AbpAuthorizationModule), typeof(AbpAuthorizationModule),
typeof(AbpEntityFrameworkCoreModule), typeof(AbpEntityFrameworkCoreModule),
typeof(AbpTextTemplatingRazorModule), typeof(AbpTextTemplatingRazorModule),
typeof(ErpSenderModule) typeof(SozsoftSenderModule)
)] )]
public class ErpMailQueueModule : AbpModule public class SozsoftMailQueueModule : AbpModule
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
context.Services.AddMapperlyObjectMapper<ErpMailQueueModule>(); context.Services.AddMapperlyObjectMapper<SozsoftMailQueueModule>();
Configure<AbpLocalizationOptions>(options => Configure<AbpLocalizationOptions>(options =>
{ {
options.Resources.Add<ErpMailQueueResource>("en"); options.Resources.Add<SozsoftMailQueueResource>("en");
}); });
Configure<AbpExceptionLocalizationOptions>(options => Configure<AbpExceptionLocalizationOptions>(options =>
{ {
options.MapCodeNamespace("MailQueue", typeof(ErpMailQueueResource)); options.MapCodeNamespace("MailQueue", typeof(SozsoftMailQueueResource));
}); });
context.Services.AddAbpDbContext<MailQueueDbContext>(options => context.Services.AddAbpDbContext<MailQueueDbContext>(options =>
@ -51,7 +51,7 @@ public class ErpMailQueueModule : AbpModule
Configure<AbpRazorTemplateCSharpCompilerOptions>(options => Configure<AbpRazorTemplateCSharpCompilerOptions>(options =>
{ {
options.References.Add(MetadataReference.CreateFromFile(typeof(ErpMailQueueModule).Assembly.Location)); options.References.Add(MetadataReference.CreateFromFile(typeof(SozsoftMailQueueModule).Assembly.Location));
}); });
Configure<AbpCompiledViewProviderOptions>(options => Configure<AbpCompiledViewProviderOptions>(options =>
@ -69,7 +69,7 @@ public class ErpMailQueueModule : AbpModule
// FileGenerationModule // FileGenerationModule
Configure<AbpVirtualFileSystemOptions>(options => Configure<AbpVirtualFileSystemOptions>(options =>
{ {
options.FileSets.AddEmbedded<ErpMailQueueModule>(); options.FileSets.AddEmbedded<SozsoftMailQueueModule>();
}); });
context.Services.AddWkhtmltopdf($"FileGeneration{Path.DirectorySeparatorChar}Rotativa"); context.Services.AddWkhtmltopdf($"FileGeneration{Path.DirectorySeparatorChar}Rotativa");

View file

@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services; using Sozsoft.Mcp.Services;
using Sozsoft.Platform; using Sozsoft.Platform;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
@ -265,9 +266,7 @@ public partial class McpQueryAppService(
private async Task<List<McpSavedQueryDto>> GetDatabaseObjectsAsync() private async Task<List<McpSavedQueryDto>> GetDatabaseObjectsAsync()
{ {
var context = await queryExecutor.ResolveDataSourceAsync(); var context = await queryExecutor.ResolveDataSourceAsync();
var catalogSql = context.DataSourceType == DataSourceTypeEnum.Postgresql var catalogSql = SqlCatalogQueries.NativeObjects(context.DataSourceType);
? McpCatalogQueries.Postgres
: McpCatalogQueries.SqlServer;
// Katalog sorgusunun kendi satir siniri yoktur; nesne sayisi tavani asmaz. // Katalog sorgusunun kendi satir siniri yoktur; nesne sayisi tavani asmaz.
var catalog = await queryExecutor.ExecuteAsync(context, catalogSql, new Dictionary<string, object>(), int.MaxValue); var catalog = await queryExecutor.ExecuteAsync(context, catalogSql, new Dictionary<string, object>(), int.MaxValue);
@ -322,7 +321,7 @@ public partial class McpQueryAppService(
var context = await queryExecutor.ResolveDataSourceAsync(); var context = await queryExecutor.ResolveDataSourceAsync();
var schemaName = string.IsNullOrWhiteSpace(input.SchemaName) var schemaName = string.IsNullOrWhiteSpace(input.SchemaName)
? sqlGuard.DefaultSchemaName(context.DataSourceType) ? SqlIdentifier.DefaultSchema(context.DataSourceType)
: input.SchemaName; : input.SchemaName;
if (!sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType).TryGetValue(out var qualifiedName, out problem) || if (!sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType).TryGetValue(out var qualifiedName, out problem) ||

View file

@ -48,20 +48,9 @@ public class McpSchemaAppService(
CurrentTenant.IsAvailable, CurrentTenant.IsAvailable,
config?.AllowedTools ?? []); config?.AllowedTools ?? []);
var required = candidates var granted = await permissionChecker.GetGrantedNamesAsync(candidates
.Where(McpToolNames.RequiredPermissions.ContainsKey) .Where(McpToolNames.RequiredPermissions.ContainsKey)
.Select(tool => McpToolNames.RequiredPermissions[tool]) .Select(tool => McpToolNames.RequiredPermissions[tool]));
.Distinct(StringComparer.Ordinal)
.ToArray();
var granted = new HashSet<string>(StringComparer.Ordinal);
if (required.Length > 0)
{
var grants = await permissionChecker.IsGrantedAsync(required);
granted.UnionWith(grants.Result
.Where(result => result.Value == PermissionGrantResult.Granted)
.Select(result => result.Key));
}
return [.. candidates.Where(tool => return [.. candidates.Where(tool =>
!McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))]; !McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))];

View file

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;
namespace Sozsoft.Mcp;
public static class PermissionCheckerExtensions
{
/// <summary>
/// Verilen yetki adlarından kullanıcıya açık olanlar; tek toplu kontrol yapılır ve sonuç
/// adla sorgulanabilen küme olarak döner. Araç listesi, ekran süzgeci ve ret mesajı aynı kümeyi okur.
/// </summary>
public static async Task<HashSet<string>> GetGrantedNamesAsync(
this IPermissionChecker permissionChecker,
IEnumerable<string> names)
{
var distinct = names.Distinct(StringComparer.Ordinal).ToArray();
if (distinct.Length == 0)
{
return new HashSet<string>(StringComparer.Ordinal);
}
var grants = await permissionChecker.IsGrantedAsync(distinct);
return grants.Result
.Where(result => result.Value == PermissionGrantResult.Granted)
.Select(result => result.Key)
.ToHashSet(StringComparer.Ordinal);
}
}

View file

@ -55,17 +55,7 @@ public class McpListFormAccessFilter(
} }
} }
var distinctPermissions = readPermissions.Values.Distinct(StringComparer.Ordinal).ToArray(); var granted = await permissionChecker.GetGrantedNamesAsync(readPermissions.Values);
if (distinctPermissions.Length == 0)
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
var grants = await permissionChecker.IsGrantedAsync(distinctPermissions);
var granted = grants.Result
.Where(result => result.Value == PermissionGrantResult.Granted)
.Select(result => result.Key)
.ToHashSet(StringComparer.Ordinal);
return readPermissions return readPermissions
.Where(entry => granted.Contains(entry.Value)) .Where(entry => granted.Contains(entry.Value))

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Platform;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@ -43,11 +44,6 @@ public class McpSchemaDictionary(
ICurrentTenant currentTenant) ICurrentTenant currentTenant)
: DomainService, IMcpSchemaDictionary : DomainService, IMcpSchemaDictionary
{ {
private static readonly JsonSerializerOptions DictionaryJsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private static readonly CultureInfo TurkishCulture = CultureInfo.GetCultureInfo("tr-TR"); private static readonly CultureInfo TurkishCulture = CultureInfo.GetCultureInfo("tr-TR");
public string ResolveTenantName() public string ResolveTenantName()
@ -81,7 +77,7 @@ public class McpSchemaDictionary(
return []; return [];
} }
var document = JsonSerializer.Deserialize<DictionaryDocument>(json, DictionaryJsonOptions); var document = JsonSerializer.Deserialize<DictionaryDocument>(json, PlatformJsonOptions.CaseInsensitive);
return document?.ListForms ?? []; return document?.ListForms ?? [];
} }

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@ -21,9 +22,6 @@ public interface IMcpSqlGuard
/// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary> /// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary>
McpOutcome<string> QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType); McpOutcome<string> QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType);
/// <summary>Saglayicinin varsayilan semasi.</summary>
string DefaultSchemaName(DataSourceTypeEnum dataSourceType);
/// <summary> /// <summary>
/// Satir tavanini sorgunun kendisine yazar: SQL Server'da <c>TOP (n)</c>, PostgreSQL'de /// Satir tavanini sorgunun kendisine yazar: SQL Server'da <c>TOP (n)</c>, PostgreSQL'de
/// <c>LIMIT n</c>. Sorgu kendi sinirini zaten tasiyorsa dokunulmaz. /// <c>LIMIT n</c>. Sorgu kendi sinirini zaten tasiyorsa dokunulmaz.
@ -39,11 +37,6 @@ public interface IMcpSqlGuard
public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard
{ {
/// <summary>Tek parcali SQL tanimlayicisi; sema ve nesne adlari bu kalibi gecmek zorundadir.</summary>
private static readonly Regex SafeIdentifierRegex = new(
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeParameterNameRegex = new( private static readonly Regex SafeParameterNameRegex = new(
@"^@?[A-Za-z_][A-Za-z0-9_]{0,127}$", @"^@?[A-Za-z_][A-Za-z0-9_]{0,127}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant); RegexOptions.Compiled | RegexOptions.CultureInvariant);
@ -111,20 +104,15 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
{ {
foreach (var name in (string?[])[schemaName, objectName]) foreach (var name in (string?[])[schemaName, objectName])
{ {
if (!SafeIdentifierRegex.IsMatch(name ?? string.Empty)) if (!SqlIdentifier.IsSafe(name))
{ {
return McpProblem.From(localizer, McpErrorCodes.InvalidIdentifier, name ?? string.Empty); return McpProblem.From(localizer, McpErrorCodes.InvalidIdentifier, name ?? string.Empty);
} }
} }
return dataSourceType == DataSourceTypeEnum.Postgresql return SqlIdentifier.QuoteQualified(schemaName, objectName, dataSourceType);
? $"\"{schemaName}\".\"{objectName}\""
: $"[{schemaName}].[{objectName}]";
} }
public string DefaultSchemaName(DataSourceTypeEnum dataSourceType)
=> dataSourceType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
public string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType) public string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType)
{ {
if (dataSourceType == DataSourceTypeEnum.Postgresql) if (dataSourceType == DataSourceTypeEnum.Postgresql)

View file

@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
@ -43,10 +44,6 @@ public class McpTableCreator(
private const int DefaultTextLength = 100; private const int DefaultTextLength = 100;
private const int MaxTextLength = 4000; private const int MaxTextLength = 4000;
private static readonly Regex SafeIdentifierRegex = new(
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary> /// <summary>
/// Istemcinin SQL bicimiyle yazdigi tip: <c>nvarchar(50)</c>, <c>nvarchar (MAX)</c>, /// Istemcinin SQL bicimiyle yazdigi tip: <c>nvarchar(50)</c>, <c>nvarchar (MAX)</c>,
/// <c>decimal(18, 4)</c>. Boyut tipten ayrilir; tip adi yine beyaz listeden gecer. /// <c>decimal(18, 4)</c>. Boyut tipten ayrilir; tip adi yine beyaz listeden gecer.
@ -427,7 +424,7 @@ public class McpTableCreator(
} }
private McpProblem? CheckIdentifier(string identifier) private McpProblem? CheckIdentifier(string identifier)
=> SafeIdentifierRegex.IsMatch(identifier) ? null : Problem(McpErrorCodes.InvalidIdentifier, identifier); => SqlIdentifier.IsSafe(identifier) ? null : Problem(McpErrorCodes.InvalidIdentifier, identifier);
private McpProblem Problem(string name, params object[] arguments) private McpProblem Problem(string name, params object[] arguments)
=> McpProblem.From(localizer, name, arguments); => McpProblem.From(localizer, name, arguments);

View file

@ -43,10 +43,11 @@ public class McpToolErrorTranslator(
candidates.Add(policyName); candidates.Add(policyName);
} }
var grants = await permissionChecker.IsGrantedAsync([.. candidates.Distinct(StringComparer.Ordinal)]); var granted = await permissionChecker.GetGrantedNamesAsync(candidates);
var missing = grants.Result var missing = candidates
.Where(result => result.Value != PermissionGrantResult.Granted) .Distinct(StringComparer.Ordinal)
.Select(result => McpPermissionText.Describe(localizer, result.Key)) .Where(permission => !granted.Contains(permission))
.Select(permission => McpPermissionText.Describe(localizer, permission))
.ToList(); .ToList();
var target = toolName ?? McpConsts.EndpointPath; var target = toolName ?? McpConsts.EndpointPath;

View file

@ -12,14 +12,6 @@ public partial class NotificationTypeToNotificationTypeDtoMapper : MapperBase<No
public override partial NotificationTypeDto Map(NotificationType source); public override partial NotificationTypeDto Map(NotificationType source);
public override partial void Map(NotificationType source, NotificationTypeDto destination); public override partial void Map(NotificationType source, NotificationTypeDto destination);
public override void BeforeMap(NotificationType source)
{
}
public override void AfterMap(NotificationType source, NotificationTypeDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -28,14 +20,6 @@ public partial class NotificationRuleToNotificationRuleDtoMapper : MapperBase<No
public override partial NotificationRuleDto Map(NotificationRule source); public override partial NotificationRuleDto Map(NotificationRule source);
public override partial void Map(NotificationRule source, NotificationRuleDto destination); public override partial void Map(NotificationRule source, NotificationRuleDto destination);
public override void BeforeMap(NotificationRule source)
{
}
public override void AfterMap(NotificationRule source, NotificationRuleDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -53,14 +37,6 @@ public partial class NotificationToNotificationDtoMapper : MapperBase<Notificati
[MapperIgnoreTarget(nameof(NotificationDto.CreatorFullname))] [MapperIgnoreTarget(nameof(NotificationDto.CreatorFullname))]
[MapperIgnoreTarget(nameof(NotificationDto.TenantId))] [MapperIgnoreTarget(nameof(NotificationDto.TenantId))]
public override partial void Map(Notification source, NotificationDto destination); public override partial void Map(Notification source, NotificationDto destination);
public override void BeforeMap(Notification source)
{
}
public override void AfterMap(Notification source, NotificationDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -95,12 +71,4 @@ public partial class CreateUpdateNotificationRuleDtoToNotificationRuleMapper : M
[MapperIgnoreTarget(nameof(NotificationRule.TenantId))] [MapperIgnoreTarget(nameof(NotificationRule.TenantId))]
[MapperIgnoreTarget(nameof(NotificationRule.RecipientType))] [MapperIgnoreTarget(nameof(NotificationRule.RecipientType))]
public override partial void Map(CreateUpdateNotificationRuleDto source, NotificationRule destination); public override partial void Map(CreateUpdateNotificationRuleDto source, NotificationRule destination);
public override void BeforeMap(CreateUpdateNotificationRuleDto source)
{
}
public override void AfterMap(CreateUpdateNotificationRuleDto source, NotificationRule destination)
{
}
} }

View file

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
using Volo.Abp.Settings;
namespace Sozsoft.Notifications.Domain;
/// <summary>
/// Bildirim metninin (tarih, sayi, dil anahtari) uretildigi kultur. Istek disinda (worker) is
/// parcaciginin kulturu surec varsayilanidir; alicinin kendi dil tercihi platformda saklanmaz
/// (dil istekte <c>Accept-Language</c> ile gelir), bu yuzden ulasilabilen en yakin deger
/// kiracinin <c>Abp.Localization.DefaultLanguage</c> ayaridir (kiraci → global → varsayilan).
/// </summary>
public interface INotificationCulture
{
Task<IDisposable> UseAsync();
}
public class NotificationCulture(ISettingProvider settingProvider) : INotificationCulture, ITransientDependency
{
public async Task<IDisposable> UseAsync()
{
var culture = await settingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage);
return culture.IsNullOrWhiteSpace() || !CultureHelper.IsValidCultureCode(culture!)
? NullDisposable.Instance
: CultureHelper.Use(culture!);
}
}

View file

@ -70,6 +70,7 @@ public class NotificationManager : INotificationManager
private readonly INotificationRealtimeNotifier realtimeNotifier; private readonly INotificationRealtimeNotifier realtimeNotifier;
private readonly ICurrentTenant currentTenant; private readonly ICurrentTenant currentTenant;
private readonly IClock clock; private readonly IClock clock;
private readonly INotificationCulture notificationCulture;
public NotificationManager( public NotificationManager(
IReadOnlyRepository<NotificationRule> repository, IReadOnlyRepository<NotificationRule> repository,
@ -80,7 +81,8 @@ public class NotificationManager : INotificationManager
IOrganizationUnitRepository repositoryOrganizationUnit, IOrganizationUnitRepository repositoryOrganizationUnit,
INotificationRealtimeNotifier realtimeNotifier, INotificationRealtimeNotifier realtimeNotifier,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IClock clock IClock clock,
INotificationCulture notificationCulture
) )
{ {
this.repository = repository; this.repository = repository;
@ -92,6 +94,7 @@ public class NotificationManager : INotificationManager
this.realtimeNotifier = realtimeNotifier; this.realtimeNotifier = realtimeNotifier;
this.currentTenant = currentTenant; this.currentTenant = currentTenant;
this.clock = clock; this.clock = clock;
this.notificationCulture = notificationCulture;
} }
public async Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage) public async Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage)
@ -208,6 +211,11 @@ public class NotificationManager : INotificationManager
.ToListAsync(); .ToListAsync();
var notifications = new List<Notification>(); var notifications = new List<Notification>();
// {Date}/{Time} yer tutuculari gonderenin istek kulturunde degil kiracinin dilinde yazilir;
// alicinin kendi dil tercihi platformda saklanmadigi icin alici basina ayrim yapilamaz.
using (await notificationCulture.UseAsync())
{
foreach (var channel in channels) foreach (var channel in channels)
{ {
foreach (var user in users) foreach (var user in users)
@ -234,6 +242,7 @@ public class NotificationManager : INotificationManager
}); });
} }
} }
}
result.SentCount = notifications.Count; result.SentCount = notifications.Count;
await SaveAndAnnounceAsync(notifications); await SaveAndAnnounceAsync(notifications);

View file

@ -3,7 +3,6 @@
namespace Sozsoft.Sender.Localization; namespace Sozsoft.Sender.Localization;
[LocalizationResourceName("SozsoftSender")] [LocalizationResourceName("SozsoftSender")]
public class ErpSenderResource public class SozsoftMailingResource
{ {
} }

View file

@ -63,7 +63,7 @@ public class AmazonSesEmailSender : EmailSenderBase, ISozsoftEmailSender, ITrans
} }
await BackgroundJobManager.EnqueueAsync( await BackgroundJobManager.EnqueueAsync(
new ErpBackgroundEmailSendingJobArgs new SozsoftBackgroundEmailSendingJobArgs
{ {
TenantId = CurrentTenant.Id, TenantId = CurrentTenant.Id,
To = to, To = to,
@ -76,7 +76,7 @@ public class AmazonSesEmailSender : EmailSenderBase, ISozsoftEmailSender, ITrans
); );
} }
public async Task<EmailSendResult> SendEmailAsync( public async Task<SendResult> SendEmailAsync(
string[] to, string[] to,
KeyValuePair<string, string>? sender, KeyValuePair<string, string>? sender,
dynamic? @params, dynamic? @params,
@ -134,7 +134,7 @@ public class AmazonSesEmailSender : EmailSenderBase, ISozsoftEmailSender, ITrans
using var client = await amazonSesClientFactory.GetAmazonSesClient(); using var client = await amazonSesClientFactory.GetAmazonSesClient();
var response = await client.SendEmailAsync(request); var response = await client.SendEmailAsync(request);
var result = new EmailSendResult(); var result = new SendResult();
if (response == null) if (response == null)
{ {
result.Success = false; result.Success = false;
@ -157,7 +157,7 @@ public class AmazonSesEmailSender : EmailSenderBase, ISozsoftEmailSender, ITrans
return result; return result;
} }
public async Task<EmailSendResult> SendEmailAsync( public async Task<SendResult> SendEmailAsync(
string to, string to,
KeyValuePair<string, string>? sender, KeyValuePair<string, string>? sender,
dynamic? @params, dynamic? @params,

View file

@ -1,9 +0,0 @@
namespace Sozsoft.Sender.Mail;
public class EmailSendResult
{
public bool Success { get; set; }
public string MessageId { get; set; } //Eger basarili ise gateway messageId
public string ErrorMessage { get; set; } //Basarisiz ise hata mesaji
}

View file

@ -7,8 +7,7 @@ namespace Sozsoft.Sender.Mail;
/// </summary> /// </summary>
public interface ISozsoftEmailSender : IEmailSender public interface ISozsoftEmailSender : IEmailSender
{ {
//ErpEmailTemplate template Task<SendResult> SendEmailAsync(
Task<EmailSendResult> SendEmailAsync(
string to, string to,
KeyValuePair<string, string>? sender, KeyValuePair<string, string>? sender,
dynamic? @params, dynamic? @params,
@ -16,7 +15,7 @@ public interface ISozsoftEmailSender : IEmailSender
string? subject = null, string? subject = null,
Dictionary<string, string>? attachments = null, Dictionary<string, string>? attachments = null,
bool IsBodyHtml = false); bool IsBodyHtml = false);
Task<EmailSendResult> SendEmailAsync( Task<SendResult> SendEmailAsync(
string[] to, string[] to,
KeyValuePair<string, string>? sender, KeyValuePair<string, string>? sender,
dynamic? @params, dynamic? @params,

View file

@ -4,13 +4,13 @@ using Volo.Abp.MultiTenancy;
namespace Sozsoft.Sender.Mail; namespace Sozsoft.Sender.Mail;
public class ErpBackgroundEmailSendingJob : public class SozsoftBackgroundEmailSendingJob :
AsyncBackgroundJob<ErpBackgroundEmailSendingJobArgs>, ITransientDependency AsyncBackgroundJob<SozsoftBackgroundEmailSendingJobArgs>, ITransientDependency
{ {
protected ISozsoftEmailSender EmailSender { get; } protected ISozsoftEmailSender EmailSender { get; }
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
public ErpBackgroundEmailSendingJob( public SozsoftBackgroundEmailSendingJob(
ISozsoftEmailSender emailSender, ISozsoftEmailSender emailSender,
ICurrentTenant currentTenant) ICurrentTenant currentTenant)
{ {
@ -18,11 +18,10 @@ public class ErpBackgroundEmailSendingJob :
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
} }
public override async Task ExecuteAsync(ErpBackgroundEmailSendingJobArgs args) public override async Task ExecuteAsync(SozsoftBackgroundEmailSendingJobArgs args)
{ {
using (CurrentTenant.Change(args.TenantId)) using (CurrentTenant.Change(args.TenantId))
{ {
//await EmailSender.SendEmailAsync(args.Template, args.To, args.Params, args.Subject);
await EmailSender.SendEmailAsync( await EmailSender.SendEmailAsync(
args.To, args.To,
args.Sender, args.Sender,

View file

@ -3,7 +3,7 @@
namespace Sozsoft.Sender.Mail; namespace Sozsoft.Sender.Mail;
[Serializable] [Serializable]
public class ErpBackgroundEmailSendingJobArgs : IMultiTenant public class SozsoftBackgroundEmailSendingJobArgs : IMultiTenant
{ {
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }

View file

@ -4,6 +4,6 @@ namespace Sozsoft.Sender.Rocket;
public interface ISozsoftRocketSender : ITransientDependency public interface ISozsoftRocketSender : ITransientDependency
{ {
Task<RocketSendResult> SendAsync(string Username, string Message); Task<SendResult> SendAsync(string Username, string Message);
} }

View file

@ -1,46 +1,38 @@
using System.Net.Http.Json; using Volo.Abp.Settings;
using Volo.Abp.Settings;
namespace Sozsoft.Sender.Rocket; namespace Sozsoft.Sender.Rocket;
public interface IRocketHttpClient public interface IRocketHttpClient
{ {
Task<RocketSendResultDto> SendMessageAsync(string Username, string Message); Task<SendResult?> SendMessageAsync(string Username, string Message);
} }
public sealed class RocketHttpClient : IRocketHttpClient, IDisposable public sealed class RocketHttpClient : SettingHttpClientBase, IRocketHttpClient
{ {
private readonly HttpClient httpClient; public RocketHttpClient(HttpClient httpClient, ISettingProvider settingProvider)
private readonly ISettingProvider settingProvider; : base(httpClient, settingProvider)
public RocketHttpClient(
HttpClient httpClient,
ISettingProvider settingProvider)
{ {
this.httpClient = httpClient;
this.settingProvider = settingProvider;
} }
public async Task<RocketSendResultDto> SendMessageAsync(string Username, string Message) public async Task<SendResult?> SendMessageAsync(string Username, string Message)
{ {
var url = await settingProvider.GetOrNullAsync(RocketSettingNames.Url); var url = await GetSettingAsync(RocketSettingNames.Url);
var userId = await settingProvider.GetOrNullAsync(RocketSettingNames.UserId); var userId = await GetSettingAsync(RocketSettingNames.UserId);
var token = await settingProvider.GetOrNullAsync(RocketSettingNames.Token); var token = await GetSettingAsync(RocketSettingNames.Token);
var request = new HttpRequestMessage(HttpMethod.Post, $"{url}/chat.postMessage"); // Rocket.Chat cevabı yalnızca { success } taşır; SendResult aynı sözleşmedir, ayrı DTO gerekmez.
request.Headers.Add("X-Auth-Token", token); return await PostJsonAsync<SendResult>(
request.Headers.Add("X-User-Id", userId); $"{url}/chat.postMessage",
request.Content = JsonContent.Create(new RocketChatPostMessageInput new RocketChatPostMessageInput
{ {
Channel = $"@{Username}", Channel = $"@{Username}",
Text = Message Text = Message
}); },
request =>
var response = await httpClient.SendAsync(request); {
response.EnsureSuccessStatusCode(); // Hata durumunda istisna atar. request.Headers.Add("X-Auth-Token", token);
return await response.Content.ReadFromJsonAsync<RocketSendResultDto>(); request.Headers.Add("X-User-Id", userId);
},
ensureSuccess: true);
} }
public void Dispose() => httpClient?.Dispose();
} }

View file

@ -1,7 +0,0 @@
namespace Sozsoft.Sender.Rocket;
public class RocketSendResult
{
public bool Success { get; set; }
}

View file

@ -1,7 +0,0 @@
namespace Sozsoft.Sender.Rocket;
public class RocketSendResultDto
{
public bool Success { get; set; }
}

View file

@ -15,11 +15,12 @@ public class RocketSender : ISozsoftRocketSender
this.logger = logger; this.logger = logger;
} }
public async Task<RocketSendResult> SendAsync(string Username, string Message) public async Task<SendResult> SendAsync(string Username, string Message)
{ {
logger.LogInformation("Sending Rocket message. {Username}, {Message}", Username, Message); logger.LogInformation("Sending Rocket message. {Username}, {Message}", Username, Message);
var result = await httpClient.SendMessageAsync(Username, Message); var result = await httpClient.SendMessageAsync(Username, Message);
if (result.Success) var success = result?.Success == true;
if (success)
{ {
logger.LogInformation("Rocket sent successfully. {Username}, {Message}", Username, Message); logger.LogInformation("Rocket sent successfully. {Username}, {Message}", Username, Message);
} }
@ -28,10 +29,6 @@ public class RocketSender : ISozsoftRocketSender
logger.LogWarning("Failed to send Rocket message. {Username}, {Message}", Username, Message); logger.LogWarning("Failed to send Rocket message. {Username}, {Message}", Username, Message);
} }
return new RocketSendResult return new SendResult { Success = success };
{
Success = result.Success,
};
} }
} }

View file

@ -0,0 +1,16 @@
namespace Sozsoft.Sender;
/// <summary>
/// Her kanalın (e-posta, SMS, Rocket, WhatsApp, Telegram) tek gönderim sonucu. Kanal yalnızca
/// başarı bilgisi veriyorsa <see cref="MessageId"/> ve <see cref="ErrorMessage"/> boş kalır.
/// </summary>
public class SendResult
{
public bool Success { get; set; }
/// <summary>Başarılı gönderimde sağlayıcının verdiği ileti kimliği.</summary>
public string? MessageId { get; set; }
/// <summary>Başarısız gönderimde sağlayıcının açıklaması.</summary>
public string? ErrorMessage { get; set; }
}

View file

@ -0,0 +1,52 @@
using System.Net.Http.Json;
using Volo.Abp.Settings;
namespace Sozsoft.Sender;
/// <summary>
/// Sağlayıcı istemcilerinin ortak iskeleti: <see cref="IHttpClientFactory"/> ile verilen
/// <see cref="HttpClient"/>, ayarlardan okunan uç nokta ve JSON gövde ile POST. Sağlayıcıya özgü
/// olan yalnızca istek gövdesi, başlıklar ve cevap tipidir.
/// </summary>
public abstract class SettingHttpClientBase : IDisposable
{
protected HttpClient HttpClient { get; }
protected ISettingProvider SettingProvider { get; }
protected SettingHttpClientBase(HttpClient httpClient, ISettingProvider settingProvider)
{
HttpClient = httpClient;
SettingProvider = settingProvider;
}
protected Task<string?> GetSettingAsync(string name) => SettingProvider.GetOrNullAsync(name);
/// <summary>
/// Gövdeyi JSON olarak gönderir ve cevabı <typeparamref name="TResponse"/> olarak okur.
/// <paramref name="ensureSuccess"/> kapalıyken 4xx gövdesi de okunur: bazı API'ler hatayı
/// durum koduyla değil gövdedeki açıklamayla verir.
/// </summary>
protected async Task<TResponse?> PostJsonAsync<TResponse>(
string url,
object body,
Action<HttpRequestMessage>? configureRequest = null,
bool ensureSuccess = false)
{
using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent.Create(body) };
configureRequest?.Invoke(request);
using var response = await HttpClient.SendAsync(request);
if (ensureSuccess)
{
response.EnsureSuccessStatusCode();
}
return await response.Content.ReadFromJsonAsync<TResponse>();
}
public void Dispose()
{
HttpClient.Dispose();
GC.SuppressFinalize(this);
}
}

View file

@ -5,6 +5,6 @@ namespace Sozsoft.Sender.Sms;
public interface ISozsoftSmsSender : ISmsSender, ITransientDependency public interface ISozsoftSmsSender : ISmsSender, ITransientDependency
{ {
Task<SmsSendResult> SendAsync(string PhoneNumber, string Message); Task<SendResult> SendAsync(string PhoneNumber, string Message);
} }

View file

@ -7,61 +7,51 @@ public interface IPostaGuverciniHttpClient
Task<PostaGuverciniSmsSendResultDto> SendSmsAsync(string PhoneNumber, string Message); Task<PostaGuverciniSmsSendResultDto> SendSmsAsync(string PhoneNumber, string Message);
} }
public sealed class PostaGuverciniHttpClient : IPostaGuverciniHttpClient, IDisposable public sealed class PostaGuverciniHttpClient : SettingHttpClientBase, IPostaGuverciniHttpClient
{ {
private readonly HttpClient httpClient; public PostaGuverciniHttpClient(HttpClient httpClient, ISettingProvider settingProvider)
private readonly ISettingProvider settingProvider; : base(httpClient, settingProvider)
public PostaGuverciniHttpClient(
HttpClient httpClient,
ISettingProvider settingProvider)
{ {
this.httpClient = httpClient;
this.settingProvider = settingProvider;
} }
public async Task<PostaGuverciniSmsSendResultDto> SendSmsAsync(string PhoneNumber, string Message) public async Task<PostaGuverciniSmsSendResultDto> SendSmsAsync(string PhoneNumber, string Message)
{ {
var url = await settingProvider.GetOrNullAsync(PostaGuverciniSettingNames.Url); var url = await GetSettingAsync(PostaGuverciniSettingNames.Url);
var u = await settingProvider.GetOrNullAsync(PostaGuverciniSettingNames.Username); var user = await GetSettingAsync(PostaGuverciniSettingNames.Username);
var p = await settingProvider.GetOrNullAsync(PostaGuverciniSettingNames.Password); var password = await GetSettingAsync(PostaGuverciniSettingNames.Password);
//httpClient.BaseAddress = new Uri(url);
var response = await httpClient.GetStringAsync($"{url}/sendsms.asp?user={u}&password={p}&gsm={PhoneNumber}&text={Message}"); // Sağlayıcı yalnızca sorgu dizisi kabul eder; metindeki '&' ve '=' kaçırılmazsa parametre sınırı bozulur.
// Gelen result başarı: errno=0&errtext=&message_id=EZ_2643D2B5-QCFD-40B7-E95F-4204CFCD0409&charge=-1 var response = await HttpClient.GetStringAsync(
// Gelen result hata: errno=10&errtext=HATA: Giriş bilgileri hatalı! $"{url}/sendsms.asp?user={Uri.EscapeDataString(user ?? string.Empty)}&password={Uri.EscapeDataString(password ?? string.Empty)}" +
$"&gsm={Uri.EscapeDataString(PhoneNumber)}&text={Uri.EscapeDataString(Message)}");
// Başarı: errno=0&errtext=&message_id=EZ_2643D2B5-...&charge=-1
// Hata: errno=10&errtext=HATA: Giriş bilgileri hatalı!
var result = new PostaGuverciniSmsSendResultDto(); var result = new PostaGuverciniSmsSendResultDto();
try
{
var options = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; var options = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
foreach (var item in response.Split('&', options)) foreach (var item in response.Split('&', options))
{ {
var token = item.Split('=', options); var token = item.Split('=', 2, options);
var value = token.Length == 2 ? token[1] : null;
switch (token[0]) switch (token[0])
{ {
case "errno": case "errno":
result.ErrNo = token.Length == 2 ? token[1] : null; result.ErrNo = value;
break; break;
case "errtext": case "errtext":
result.ErrText = token.Length == 2 ? token[1] : null; result.ErrText = value;
break; break;
case "message_id": case "message_id":
result.MessageId = token.Length == 2 ? token[1] : null; result.MessageId = value;
break; break;
case "charge": case "charge":
result.Charge = token.Length == 2 ? token[1] : null; result.Charge = value;
break; break;
default: default:
break; break;
} }
} }
}
catch
{
}
return result; return result;
} }
public void Dispose() => httpClient?.Dispose();
} }

View file

@ -19,7 +19,7 @@ public class PostaGuverciniSmsSender : ISozsoftSmsSender
public Task SendAsync(SmsMessage smsMessage) => SendAsync(smsMessage.PhoneNumber, smsMessage.Text); public Task SendAsync(SmsMessage smsMessage) => SendAsync(smsMessage.PhoneNumber, smsMessage.Text);
public async Task<SmsSendResult> SendAsync(string PhoneNumber, string Message) public async Task<SendResult> SendAsync(string PhoneNumber, string Message)
{ {
PhoneNumber = Check.NotNullOrWhiteSpace(PhoneNumber, nameof(PhoneNumber), 11, 10); PhoneNumber = Check.NotNullOrWhiteSpace(PhoneNumber, nameof(PhoneNumber), 11, 10);
Message = Check.NotNullOrWhiteSpace(Message, nameof(Message)); Message = Check.NotNullOrWhiteSpace(Message, nameof(Message));
@ -36,7 +36,7 @@ public class PostaGuverciniSmsSender : ISozsoftSmsSender
logger.LogWarning("Failed to send SMS message. {PhoneNumber}, {Message}, {Error}", PhoneNumber, Message, $"{result.ErrNo}:{result.ErrText}"); logger.LogWarning("Failed to send SMS message. {PhoneNumber}, {Message}, {Error}", PhoneNumber, Message, $"{result.ErrNo}:{result.ErrText}");
} }
return new SmsSendResult return new SendResult
{ {
Success = result.IsSuccess, Success = result.IsSuccess,
ErrorMessage = $"{result.ErrNo}:{result.ErrText}", ErrorMessage = $"{result.ErrNo}:{result.ErrText}",

View file

@ -1,9 +0,0 @@
namespace Sozsoft.Sender.Sms;
public class SmsSendResult
{
public bool Success { get; set; }
public string MessageId { get; set; } //Eger basarili ise gateway messageId
public string ErrorMessage { get; set; } //Basarisiz ise hata mesaji
}

View file

@ -18,18 +18,18 @@ namespace Sozsoft.Sender;
[DependsOn( [DependsOn(
typeof(AbpEmailingModule), typeof(AbpEmailingModule),
typeof(AbpSmsModule))] typeof(AbpSmsModule))]
public class ErpSenderModule : AbpModule public class SozsoftSenderModule : AbpModule
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<AbpLocalizationOptions>(options => Configure<AbpLocalizationOptions>(options =>
{ {
options.Resources.Add<ErpSenderResource>("en"); options.Resources.Add<SozsoftMailingResource>("en");
}); });
Configure<AbpBackgroundJobOptions>(options => Configure<AbpBackgroundJobOptions>(options =>
{ {
options.AddJob<ErpBackgroundEmailSendingJob>(); options.AddJob<SozsoftBackgroundEmailSendingJob>();
}); });
//Register a factory method that resolves from IServiceProvider //Register a factory method that resolves from IServiceProvider

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Sender.Telegram;
public interface ISozsoftTelegramSender : ITransientDependency public interface ISozsoftTelegramSender : ITransientDependency
{ {
Task<TelegramSendResult> SendAsync(string ChatId, string Message); Task<SendResult> SendAsync(string ChatId, string Message);
} }
public class SozsoftTelegramSender : ISozsoftTelegramSender public class SozsoftTelegramSender : ISozsoftTelegramSender
@ -21,7 +21,7 @@ public class SozsoftTelegramSender : ISozsoftTelegramSender
this.logger = logger; this.logger = logger;
} }
public async Task<TelegramSendResult> SendAsync(string ChatId, string Message) public async Task<SendResult> SendAsync(string ChatId, string Message)
{ {
logger.LogInformation("Sending Telegram message. {ChatId}, {Message}", ChatId, Message); logger.LogInformation("Sending Telegram message. {ChatId}, {Message}", ChatId, Message);
var result = await httpClient.SendMessageAsync(ChatId, Message); var result = await httpClient.SendMessageAsync(ChatId, Message);
@ -36,9 +36,11 @@ public class SozsoftTelegramSender : ISozsoftTelegramSender
ChatId, Message, result?.error_code, result?.description); ChatId, Message, result?.error_code, result?.description);
} }
return new TelegramSendResult return new SendResult
{ {
Success = success, Success = success,
MessageId = success ? result!.result?.message_id.ToString() : null,
ErrorMessage = success ? null : result?.description,
}; };
} }
} }

View file

@ -1,33 +1,28 @@
using System.Net.Http.Json;
using Volo.Abp.Settings; using Volo.Abp.Settings;
namespace Sozsoft.Sender.Telegram; namespace Sozsoft.Sender.Telegram;
public interface ITelegramHttpClient public interface ITelegramHttpClient
{ {
Task<TelegramSendResultDto> SendMessageAsync(string ChatId, string Message); Task<TelegramSendResultDto?> SendMessageAsync(string ChatId, string Message);
} }
public sealed class TelegramHttpClient : ITelegramHttpClient, IDisposable public sealed class TelegramHttpClient : SettingHttpClientBase, ITelegramHttpClient
{ {
private readonly HttpClient httpClient; public TelegramHttpClient(HttpClient httpClient, ISettingProvider settingProvider)
private readonly ISettingProvider settingProvider; : base(httpClient, settingProvider)
public TelegramHttpClient(
HttpClient httpClient,
ISettingProvider settingProvider)
{ {
this.httpClient = httpClient;
this.settingProvider = settingProvider;
} }
public async Task<TelegramSendResultDto> SendMessageAsync(string ChatId, string Message) public async Task<TelegramSendResultDto?> SendMessageAsync(string ChatId, string Message)
{ {
var url = await settingProvider.GetOrNullAsync(TelegramSettingNames.Url); var url = await GetSettingAsync(TelegramSettingNames.Url);
var botToken = await settingProvider.GetOrNullAsync(TelegramSettingNames.BotToken); var botToken = await GetSettingAsync(TelegramSettingNames.BotToken);
var parseMode = await settingProvider.GetOrNullAsync(TelegramSettingNames.ParseMode); var parseMode = await GetSettingAsync(TelegramSettingNames.ParseMode);
var response = await httpClient.PostAsJsonAsync($"{url.TrimEnd('/')}/bot{botToken}/sendMessage", // Bot API hatayı 4xx gövdesinde açıklar; durum kodunu fırlatmak açıklamayı kaybettirir.
return await PostJsonAsync<TelegramSendResultDto>(
$"{url?.TrimEnd('/')}/bot{botToken}/sendMessage",
new TelegramPostMessageInput new TelegramPostMessageInput
{ {
chat_id = ChatId, chat_id = ChatId,
@ -35,10 +30,5 @@ public sealed class TelegramHttpClient : ITelegramHttpClient, IDisposable
parse_mode = parseMode.IsNullOrWhiteSpace() ? null : parseMode, parse_mode = parseMode.IsNullOrWhiteSpace() ? null : parseMode,
disable_web_page_preview = true disable_web_page_preview = true
}); });
// Bot API hatayı 4xx gövdesinde açıklar; durum kodunu fırlatmak açıklamayı kaybettirir.
return await response.Content.ReadFromJsonAsync<TelegramSendResultDto>();
} }
public void Dispose() => httpClient?.Dispose();
} }

View file

@ -1,6 +0,0 @@
namespace Sozsoft.Sender.Telegram;
public class TelegramSendResult
{
public bool Success { get; set; }
}

View file

@ -5,7 +5,7 @@ namespace Sozsoft.Sender.WhatsApp;
public interface ISozsoftWhatsAppSender : ITransientDependency public interface ISozsoftWhatsAppSender : ITransientDependency
{ {
Task<WhatsAppSendResult> SendAsync(string To, string Message, string LanguageCode = "tr"); Task<SendResult> SendAsync(string To, string Message, string LanguageCode = "tr");
} }
public class SozsoftWhatsAppSender : ISozsoftWhatsAppSender public class SozsoftWhatsAppSender : ISozsoftWhatsAppSender
@ -21,11 +21,12 @@ public class SozsoftWhatsAppSender : ISozsoftWhatsAppSender
this.logger = logger; this.logger = logger;
} }
public async Task<WhatsAppSendResult> SendAsync(string To, string Message, string LanguageCode = "tr") public async Task<SendResult> SendAsync(string To, string Message, string LanguageCode = "tr")
{ {
logger.LogInformation("Sending WhatsApp message. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message); logger.LogInformation("Sending WhatsApp message. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message);
var result = await httpClient.SendMessageAsync(To, Message, LanguageCode); var result = await httpClient.SendMessageAsync(To, Message, LanguageCode);
var success = !result.messages.IsNullOrEmpty() && result.messages.FirstOrDefault()?.message_status == "accepted"; var message = result?.messages?.FirstOrDefault();
var success = message?.message_status == "accepted";
if (success) if (success)
{ {
logger.LogInformation("WhatsApp sent successfully. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message); logger.LogInformation("WhatsApp sent successfully. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message);
@ -35,10 +36,10 @@ public class SozsoftWhatsAppSender : ISozsoftWhatsAppSender
logger.LogWarning("Failed to send WhatsApp message. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message); logger.LogWarning("Failed to send WhatsApp message. {To}, {LanguageCode}, {Message}", To, LanguageCode, Message);
} }
return new WhatsAppSendResult return new SendResult
{ {
Success = success, Success = success,
MessageId = success ? message!.id : null,
}; };
} }
} }

View file

@ -1,36 +1,29 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json;
using Volo.Abp.Settings; using Volo.Abp.Settings;
namespace Sozsoft.Sender.WhatsApp; namespace Sozsoft.Sender.WhatsApp;
public interface IWhatsAppHttpClient public interface IWhatsAppHttpClient
{ {
Task<WhatsAppSendResultDto> SendMessageAsync(string To, string Message, string LanguageCode = "tr"); Task<WhatsAppSendResultDto?> SendMessageAsync(string To, string Message, string LanguageCode = "tr");
} }
public sealed class WhatsAppHttpClient : IWhatsAppHttpClient, IDisposable public sealed class WhatsAppHttpClient : SettingHttpClientBase, IWhatsAppHttpClient
{ {
private readonly HttpClient httpClient; public WhatsAppHttpClient(HttpClient httpClient, ISettingProvider settingProvider)
private readonly ISettingProvider settingProvider; : base(httpClient, settingProvider)
public WhatsAppHttpClient(
HttpClient httpClient,
ISettingProvider settingProvider)
{ {
this.httpClient = httpClient;
this.settingProvider = settingProvider;
} }
public async Task<WhatsAppSendResultDto> SendMessageAsync(string To, string Message, string LanguageCode = "tr") public async Task<WhatsAppSendResultDto?> SendMessageAsync(string To, string Message, string LanguageCode = "tr")
{ {
var url = await settingProvider.GetOrNullAsync(WhatsAppSettingNames.Url); var url = await GetSettingAsync(WhatsAppSettingNames.Url);
var templateName = await settingProvider.GetOrNullAsync(WhatsAppSettingNames.TemplateName); var templateName = await GetSettingAsync(WhatsAppSettingNames.TemplateName);
var token = await settingProvider.GetOrNullAsync(WhatsAppSettingNames.Token); var token = await GetSettingAsync(WhatsAppSettingNames.Token);
var phoneId = await settingProvider.GetOrNullAsync(WhatsAppSettingNames.PhoneNumberId); var phoneId = await GetSettingAsync(WhatsAppSettingNames.PhoneNumberId);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return await PostJsonAsync<WhatsAppSendResultDto>(
var response = await httpClient.PostAsJsonAsync($"{url}/{phoneId}/messages", $"{url}/{phoneId}/messages",
new WhatsAppPostMessageInput new WhatsAppPostMessageInput
{ {
messaging_product = "whatsapp", messaging_product = "whatsapp",
@ -59,10 +52,7 @@ public sealed class WhatsAppHttpClient : IWhatsAppHttpClient, IDisposable
} }
] ]
} }
}); },
return await response.Content.ReadFromJsonAsync<WhatsAppSendResultDto>(); request => request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token));
} }
public void Dispose() => httpClient?.Dispose();
} }

View file

@ -1,7 +0,0 @@
namespace Sozsoft.Sender.WhatsApp;
public class WhatsAppSendResult
{
public bool Success { get; set; }
}

View file

@ -13,27 +13,26 @@ namespace Sozsoft.Settings;
[Authorize] [Authorize]
public class SettingUiAppService : ApplicationService, ISettingUiAppService public class SettingUiAppService : ApplicationService, ISettingUiAppService
{ {
//private readonly IRepository<SettingDefinition, Guid> repositorySettingDefinition;
private readonly ISettingDefinitionManager settingDefinitionManager; private readonly ISettingDefinitionManager settingDefinitionManager;
private readonly ISettingManager settingManager; private readonly ISettingManager settingManager;
private readonly ErpSettingDefinitionManager ErpSettingDefinitionManager; private readonly SettingDefinitionManager platformSettingDefinitionManager;
private readonly ICurrentTenant currentTenant; private readonly ICurrentTenant currentTenant;
public SettingUiAppService( public SettingUiAppService(
ISettingDefinitionManager settingDefinitionManager, ISettingDefinitionManager settingDefinitionManager,
ISettingManager settingManager, ISettingManager settingManager,
ErpSettingDefinitionManager ErpSettingDefinitionManager, SettingDefinitionManager platformSettingDefinitionManager,
ICurrentTenant currentTenant) ICurrentTenant currentTenant)
{ {
this.settingDefinitionManager = settingDefinitionManager; this.settingDefinitionManager = settingDefinitionManager;
this.settingManager = settingManager; this.settingManager = settingManager;
this.ErpSettingDefinitionManager = ErpSettingDefinitionManager; this.platformSettingDefinitionManager = platformSettingDefinitionManager;
this.currentTenant = currentTenant; this.currentTenant = currentTenant;
} }
public virtual async Task<List<MainGroupedSettingDto>> GetListAsync() public virtual async Task<List<MainGroupedSettingDto>> GetListAsync()
{ {
var groups = await ErpSettingDefinitionManager.GetGroupedSettingDefinitionsAsync(); var groups = await platformSettingDefinitionManager.GetGroupedSettingDefinitionsAsync();
return groups.Select(group => new MainGroupedSettingDto return groups.Select(group => new MainGroupedSettingDto
{ {
GroupName = group.GroupName, GroupName = group.GroupName,

View file

@ -11,14 +11,6 @@ public partial class SettingDefinitionToSettingDefinitionDtoMapper : TwoWayMappe
public override partial void Map(SettingDefinition source, SettingDefinitionDto destination); public override partial void Map(SettingDefinition source, SettingDefinitionDto destination);
public override void BeforeMap(SettingDefinition source)
{
}
public override void AfterMap(SettingDefinition source, SettingDefinitionDto destination)
{
}
public override partial SettingDefinition ReverseMap(SettingDefinitionDto destination); public override partial SettingDefinition ReverseMap(SettingDefinitionDto destination);
public override partial void ReverseMap(SettingDefinitionDto destination, SettingDefinition source); public override partial void ReverseMap(SettingDefinitionDto destination, SettingDefinition source);

View file

@ -12,14 +12,14 @@ using SettingDefinition = Sozsoft.Settings.Entities.SettingDefinition;
namespace Sozsoft.Settings; namespace Sozsoft.Settings;
public class ErpSettingDefinitionManager : DomainService public class SettingDefinitionManager : DomainService
{ {
private readonly IRepository<SettingDefinition, Guid> repository; private readonly IRepository<SettingDefinition, Guid> repository;
private readonly IStringLocalizer<SettingsResource> l; private readonly IStringLocalizer<SettingsResource> l;
private readonly ISettingProvider settingProvider; private readonly ISettingProvider settingProvider;
private readonly IAuthorizationService authorizationService; private readonly IAuthorizationService authorizationService;
public ErpSettingDefinitionManager( public SettingDefinitionManager(
IRepository<SettingDefinition, Guid> repository, IRepository<SettingDefinition, Guid> repository,
IStringLocalizer<SettingsResource> l, IStringLocalizer<SettingsResource> l,
ISettingProvider settingProvider, ISettingProvider settingProvider,
@ -30,8 +30,6 @@ public class ErpSettingDefinitionManager : DomainService
this.l = l; this.l = l;
this.settingProvider = settingProvider; this.settingProvider = settingProvider;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
//TODO: Bu sekilde settingleri alip vt ile Code uzerinden kiyaslayip insert eden seeder yazalim
//var settingDefinitions = _settingDefinitionManager.GetAll();
} }
public virtual async Task<List<SettingGroupValueObject>> GetGroupedSettingDefinitionsAsync() public virtual async Task<List<SettingGroupValueObject>> GetGroupedSettingDefinitionsAsync()
@ -88,24 +86,5 @@ public class ErpSettingDefinitionManager : DomainService
return result; return result;
} }
//private async Task<bool> CheckPermissionsAsync(List<string> permissions)
//{
// //TODO: permisson tanimli degilse hata firlatma
// var results = new List<bool>();
// try
// {
// foreach (var item in permissions)
// {
// results.Add(await authorizationService.IsGrantedAsync(item));
// }
// }
// catch
// {
// results.Add(false);
// }
// return results.Any(a => a);
//}
} }

View file

@ -68,42 +68,3 @@ public class SettingsDefinitionProvider : SettingDefinitionProvider
return new LocalizableString(typeof(SettingsResource), name); return new LocalizableString(typeof(SettingsResource), name);
} }
} }
/* Definitionlar normalde eziliyor fakat bir sebepten mevcut definitionlar ezilmezse, mevcutları alıp değiştirmek için gerekli yapı şu şekilde:
var iDescription = item.DescriptionKey.IsNullOrEmpty() ? null : L(item.DescriptionKey);
var providers = item.Providers.IsNullOrEmpty() ? new string[] { } : item.Providers.ToArray();
var def = context.GetOrNull(item.Code);
if (def == null)
{
def = new Volo.Abp.Settings.SettingDefinition(
item.Code, item.DefaultValue, L(item.NameKey), iDescription,
item.IsVisibleToClients, item.IsInherited, item.IsEncrypted)
.WithProperty(SettingsConsts.MainGroup, L(item.MainGroupKey))
.WithProperty(SettingsConsts.SubGroup, L(item.SubGroupKey))
.WithProperty(SettingsConsts.DataType, item.DataType)
.WithProperty(SettingsConsts.RequiredPermission, item.RequiredPermissionName)
.WithProperty(SettingsConsts.SelectOptions, item.SelectOptions?.ToDictionary(x => x.Key, x => L(x.Value)))
.WithProviders(providers);
context.Add(def);
}
else
{
def.DefaultValue = item.DefaultValue;
def.DisplayName = L(item.NameKey);
def.Description = iDescription;
def.IsVisibleToClients = item.IsVisibleToClients;
def.IsInherited = item.IsInherited;
def.IsEncrypted = item.IsEncrypted;
def.Properties.Clear();
def.Providers.Clear();
def.WithProperty(SettingsConsts.MainGroup, L(item.MainGroupKey))
.WithProperty(SettingsConsts.SubGroup, L(item.SubGroupKey))
.WithProperty(SettingsConsts.DataType, item.DataType)
.WithProperty(SettingsConsts.RequiredPermission, item.RequiredPermissionName)
.WithProperty(SettingsConsts.SelectOptions, item.SelectOptions?.ToDictionary(x => x.Key, x => L(x.Value)))
.WithProviders(providers);
}
*/

View file

@ -9,8 +9,10 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Queries; using Sozsoft.Platform.Queries;
using Sozsoft.Settings; using Sozsoft.Settings;
using Sozsoft.SqlQueryManager.Application.Contracts; using Sozsoft.SqlQueryManager.Application.Contracts;
@ -27,8 +29,6 @@ namespace Sozsoft.SqlQueryManager.Application;
[Authorize("App.SqlQueryManager")] [Authorize("App.SqlQueryManager")]
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
{ {
private const string QueryExecutedSuccessfullyMessage = "Query executed successfully.";
private const string QueryExecutedAndDeployedMessage = "Query executed and deployed successfully";
private const string SqlIdentifierPattern = @"(?:\[[^\]]+\]|\""[^\""\r\n]+\""|[A-Za-z_][A-Za-z0-9_@$#]*)"; private const string SqlIdentifierPattern = @"(?:\[[^\]]+\]|\""[^\""\r\n]+\""|[A-Za-z_][A-Za-z0-9_@$#]*)";
private const string MultiPartSqlIdentifierPattern = SqlIdentifierPattern + @"(?:\s*\.\s*" + SqlIdentifierPattern + @"){0,2}"; private const string MultiPartSqlIdentifierPattern = SqlIdentifierPattern + @"(?:\s*\.\s*" + SqlIdentifierPattern + @"){0,2}";
@ -69,6 +69,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
LocalizationResource = typeof(PlatformResource);
} }
private string GetTenantFromHeader() private string GetTenantFromHeader()
@ -88,7 +89,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
if (headerTenant != currentTenantName) if (headerTenant != currentTenantName)
{ {
throw new Volo.Abp.UserFriendlyException($"Tenant mismatch. Header tenant '{headerTenant}' does not match current tenant '{currentTenantName}'."); throw new Volo.Abp.UserFriendlyException(L["App.SqlQueryManager.TenantMismatch", headerTenant, currentTenantName]);
} }
} }
} }
@ -100,9 +101,11 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode); var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
result.Tables = await GetTablesAsync(dataSourceCode, dataSourceType); result.Tables = await GetTablesAsync(dataSourceCode, dataSourceType);
result.Views = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "V");
result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "P"); var nativeObjects = await GetNativeObjectsAsync(dataSourceCode, dataSourceType);
result.Functions = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "FN", "IF", "TF"); result.Views = OfKinds(nativeObjects, SqlCatalogQueries.ObjectKinds.View);
result.StoredProcedures = OfKinds(nativeObjects, SqlCatalogQueries.ObjectKinds.Procedure);
result.Functions = OfKinds(nativeObjects, SqlCatalogQueries.ObjectKinds.ScalarFunction, SqlCatalogQueries.ObjectKinds.TableFunction);
result.Templates = _templateProvider.GetAvailableQueryTemplates() result.Templates = _templateProvider.GetAvailableQueryTemplates()
.Select(t => new SqlTemplateDto .Select(t => new SqlTemplateDto
@ -122,37 +125,36 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
var dataSource = await _dataSourceManager.GetDataSourceAsync(_currentTenant.IsAvailable, dataSourceCode); var dataSource = await _dataSourceManager.GetDataSourceAsync(_currentTenant.IsAvailable, dataSourceCode);
if (dataSource == null) if (dataSource == null)
{ {
throw new Volo.Abp.UserFriendlyException($"Data source '{dataSourceCode}' was not found."); throw new Volo.Abp.UserFriendlyException(L["App.SqlQueryManager.DataSourceNotFound", dataSourceCode]);
} }
return dataSource.DataSourceType; return dataSource.DataSourceType;
} }
private async Task<List<SqlNativeObjectDto>> GetNativeObjectsAsync( /// <summary>
/// Katalog bir kez okunur; tür başına liste, katalog sırasını (şema + ad) koruyarak süzülür.
/// </summary>
private async Task<List<(string Kind, SqlNativeObjectDto Dto)>> GetNativeObjectsAsync(
string dataSourceCode, string dataSourceCode,
DataSourceTypeEnum dataSourceType, DataSourceTypeEnum dataSourceType)
params string[] objectTypes)
{ {
var query = dataSourceType == DataSourceTypeEnum.Postgresql var result = await _sqlExecutorService.ExecuteQueryAsync(SqlCatalogQueries.NativeObjects(dataSourceType), dataSourceCode);
? BuildPostgreSqlNativeObjectsQuery(objectTypes)
: BuildSqlServerNativeObjectsQuery(objectTypes);
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var objects = new List<(string Kind, SqlNativeObjectDto Dto)>();
var objects = new List<SqlNativeObjectDto>();
if (result.Success && result.Data != null) if (result.Success && result.Data != null)
{ {
foreach (var row in result.Data) foreach (var row in result.Data)
{ {
var dict = row as IDictionary<string, object>; if (row is IDictionary<string, object> dict)
if (dict != null)
{ {
objects.Add(new SqlNativeObjectDto objects.Add((
GetDictionaryValue(dict, "ObjectKind")?.ToString() ?? "",
new SqlNativeObjectDto
{ {
SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType), SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? SqlIdentifier.DefaultSchema(dataSourceType),
ObjectName = GetDictionaryValue(dict, "ObjectName")?.ToString() ?? "", ObjectName = GetDictionaryValue(dict, "ObjectName")?.ToString() ?? "",
DataSourceType = dataSourceType.ToString() DataSourceType = dataSourceType.ToString()
}); }));
} }
} }
} }
@ -160,60 +162,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return objects; return objects;
} }
private static string BuildSqlServerNativeObjectsQuery(params string[] objectTypes) private static List<SqlNativeObjectDto> OfKinds(List<(string Kind, SqlNativeObjectDto Dto)> objects, params string[] kinds)
{ => [.. objects.Where(o => kinds.Contains(o.Kind)).Select(o => o.Dto)];
var typeList = string.Join(",", objectTypes.Select(t => $"'{t}'"));
return $@"
SELECT
SCHEMA_NAME(o.schema_id) AS SchemaName,
o.name AS ObjectName
FROM
sys.objects o
WHERE
o.type IN ({typeList})
AND o.is_ms_shipped = 0
ORDER BY
SCHEMA_NAME(o.schema_id), o.name";
}
private static string BuildPostgreSqlNativeObjectsQuery(params string[] objectTypes)
{
var wantsViews = objectTypes.Contains("V");
var wantsProcedures = objectTypes.Contains("P");
var wantsFunctions = objectTypes.Any(t => t is "FN" or "IF" or "TF");
if (wantsViews)
{
return @"
SELECT
table_schema AS ""SchemaName"",
table_name AS ""ObjectName""
FROM information_schema.views
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_schema, table_name";
}
var proKinds = new List<string>();
if (wantsProcedures)
{
proKinds.Add("'p'");
}
if (wantsFunctions)
{
proKinds.Add("'f'");
}
return $@"
SELECT
n.nspname AS ""SchemaName"",
p.proname AS ""ObjectName""
FROM pg_proc p
INNER JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND p.prokind IN ({string.Join(",", proKinds)})
ORDER BY n.nspname, p.proname";
}
private async Task<List<DatabaseTableDto>> GetTablesAsync(string dataSourceCode, DataSourceTypeEnum dataSourceType) private async Task<List<DatabaseTableDto>> GetTablesAsync(string dataSourceCode, DataSourceTypeEnum dataSourceType)
{ {
@ -249,7 +199,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
tables.Add(new DatabaseTableDto tables.Add(new DatabaseTableDto
{ {
SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType), SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? SqlIdentifier.DefaultSchema(dataSourceType),
TableName = GetDictionaryValue(dict, "TableName")?.ToString() ?? "", TableName = GetDictionaryValue(dict, "TableName")?.ToString() ?? "",
DataSourceType = dataSourceType.ToString() DataSourceType = dataSourceType.ToString()
}); });
@ -611,11 +561,6 @@ FROM (
return string.IsNullOrEmpty(match.Key) ? null : match.Value; return string.IsNullOrEmpty(match.Key) ? null : match.Value;
} }
private static string GetDefaultSchemaName(DataSourceTypeEnum dataSourceType)
{
return dataSourceType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
}
private static string ToSqlLiteral(string value) private static string ToSqlLiteral(string value)
{ {
return $"'{(value ?? string.Empty).Replace("'", "''")}'"; return $"'{(value ?? string.Empty).Replace("'", "''")}'";
@ -649,7 +594,7 @@ FROM (
{ {
Success = result.Success, Success = result.Success,
Message = result.Success Message = result.Success
? (isDeployed ? QueryExecutedAndDeployedMessage : QueryExecutedSuccessfullyMessage) ? L[isDeployed ? "App.SqlQueryManager.QueryExecutedAndDeployed" : "App.Platform.QueryExecutedSuccessfully"]
: result.Message, : result.Message,
Data = result.Data, Data = result.Data,
RowsAffected = result.RowsAffected, RowsAffected = result.RowsAffected,
@ -666,7 +611,7 @@ FROM (
input.FileName.Contains('\\') || input.FileName.Contains('\\') ||
input.FileName.Contains("..")) input.FileName.Contains(".."))
{ {
throw new Volo.Abp.UserFriendlyException("Invalid file name."); throw new Volo.Abp.UserFriendlyException(L["App.Platform.InvalidFileName"]);
} }
try try

View file

@ -3,6 +3,6 @@
namespace Sozsoft.SqlQueryManager.Domain.Shared; namespace Sozsoft.SqlQueryManager.Domain.Shared;
[LocalizationResourceName("SqlQueryManager")] [LocalizationResourceName("SqlQueryManager")]
public class ErpSqlQueryManagerResource public class SozsoftSqlQueryManagerResource
{ {
} }

View file

@ -23,12 +23,12 @@ public class SqlQueryManagerDomainSharedModule : AbpModule
Configure<AbpLocalizationOptions>(options => Configure<AbpLocalizationOptions>(options =>
{ {
options.Resources.Add<ErpSqlQueryManagerResource>("en"); options.Resources.Add<SozsoftSqlQueryManagerResource>("en");
}); });
Configure<AbpExceptionLocalizationOptions>(options => Configure<AbpExceptionLocalizationOptions>(options =>
{ {
options.MapCodeNamespace("SqlQueryManager", typeof(ErpSqlQueryManagerResource)); options.MapCodeNamespace("SqlQueryManager", typeof(SozsoftSqlQueryManagerResource));
}); });
} }
} }

View file

@ -5,8 +5,10 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Queries; using Sozsoft.Platform.Queries;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@ -17,13 +19,16 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
{ {
private readonly IDataSourceManager _dataSourceManager; private readonly IDataSourceManager _dataSourceManager;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IStringLocalizer<PlatformResource> _localizer;
public SqlExecutorService( public SqlExecutorService(
IDataSourceManager dataSourceManager, IDataSourceManager dataSourceManager,
IServiceProvider serviceProvider) IServiceProvider serviceProvider,
IStringLocalizer<PlatformResource> localizer)
{ {
_dataSourceManager = dataSourceManager; _dataSourceManager = dataSourceManager;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_localizer = localizer;
} }
private async Task<IDynamicDataRepository> GetRepositoryAsync(string dataSourceCode) private async Task<IDynamicDataRepository> GetRepositoryAsync(string dataSourceCode)
@ -92,13 +97,13 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
result.Data = data; result.Data = data;
result.RowsAffected = data?.Count() ?? 0; result.RowsAffected = data?.Count() ?? 0;
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = $"Query executed successfully."; result.Message = _localizer["App.Platform.QueryExecutedSuccessfully"];
} }
catch (Exception ex) catch (Exception ex)
{ {
stopwatch.Stop(); stopwatch.Stop();
result.Success = false; result.Success = false;
result.Message = $"Query execution failed: {ex.Message}"; result.Message = _localizer["App.SqlQueryManager.QueryExecutionFailed", ex.Message];
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString(); result.Metadata["ErrorDetail"] = ex.ToString();
} }
@ -134,13 +139,13 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
result.Success = true; result.Success = true;
result.RowsAffected = rowsAffected; result.RowsAffected = rowsAffected;
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = $"Command executed successfully. Rows affected: {rowsAffected}"; result.Message = _localizer["App.SqlQueryManager.CommandExecuted", rowsAffected];
} }
catch (Exception ex) catch (Exception ex)
{ {
stopwatch.Stop(); stopwatch.Stop();
result.Success = false; result.Success = false;
result.Message = $"Command execution failed: {ex.Message}"; result.Message = _localizer["App.SqlQueryManager.CommandExecutionFailed", ex.Message];
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString(); result.Metadata["ErrorDetail"] = ex.ToString();
} }
@ -176,14 +181,14 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
result.Success = true; result.Success = true;
result.Data = new[] { new { Value = scalarValue } }; result.Data = new[] { new { Value = scalarValue } };
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = "Scalar query executed successfully"; result.Message = _localizer["App.SqlQueryManager.ScalarQueryExecuted"];
result.Metadata["ScalarValue"] = scalarValue; result.Metadata["ScalarValue"] = scalarValue;
} }
catch (Exception ex) catch (Exception ex)
{ {
stopwatch.Stop(); stopwatch.Stop();
result.Success = false; result.Success = false;
result.Message = $"Scalar query execution failed: {ex.Message}"; result.Message = _localizer["App.SqlQueryManager.ScalarQueryFailed", ex.Message];
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString(); result.Metadata["ErrorDetail"] = ex.ToString();
} }

View file

@ -1,13 +0,0 @@
using System;
namespace Sozsoft.Platform.Dashboard;
public class DashboardWidgetsDto
{
public int Id { get; set; }
public string Label { get; set; }
public Single? DataValue { get; set; }
public Single? DataGrowShrink { get; set; }
public string ValuePrefix { get; set; }
public DateTime Date { get; set; }
}

View file

@ -1,9 +0,0 @@
using System;
namespace Sozsoft.Platform.Dashboard;
public class DashboardWidgetsRequestDto
{
public DateTime Tarih1 { get; set; }
public DateTime Tarih2 { get; set; }
}

View file

@ -30,7 +30,7 @@ public class CreateUpdateCustomComponentDto
/// baska bir ekranin icinde cizilen alt bilgi) hicbir adrese baglanmaz. /// baska bir ekranin icinde cizilen alt bilgi) hicbir adrese baglanmaz.
/// </summary> /// </summary>
[StringLength(512)] [StringLength(512)]
[RegularExpression(@"^(/.*)?$", ErrorMessage = "RoutePath must start with '/'.")] [RegularExpression(@"^(/.*)?$", ErrorMessage = "App.Validation.RoutePathMustStartWithSlash")]
public string? RoutePath { get; set; } public string? RoutePath { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string? Props { get; set; } public string? Props { get; set; }

View file

@ -8,7 +8,7 @@ public class ChartAnimationDto
{ {
/// <summary> Animasyonun milisaniye cinsinden ne kadar süreceğini belirtir. /// <summary> Animasyonun milisaniye cinsinden ne kadar süreceğini belirtir.
/// </summary> /// </summary>
[Range(100, 2500, ErrorMessage = "Value for {0} must be between {1} and {2}.")] [Range(100, 2500, ErrorMessage = "App.Validation.RangeBetween")]
public int Duration { get; set; } = 1000; public int Duration { get; set; } = 1000;
/// <summary> Animasyonun kolaylaştırma işlevini belirtir. /// <summary> Animasyonun kolaylaştırma işlevini belirtir.
/// Accepted Values: 'easeOutCubic' | 'linear' /// Accepted Values: 'easeOutCubic' | 'linear'

View file

@ -86,21 +86,4 @@ public class ChartAnnotationDto
/// <summary> Ek açıklamanın merkezini belirli bir piksel koordinatına konumlandırmak için x ile birlikte kullanılır. (0, 0), UI bileşeninin sol üst köşesidir. /// <summary> Ek açıklamanın merkezini belirli bir piksel koordinatına konumlandırmak için x ile birlikte kullanılır. (0, 0), UI bileşeninin sol üst köşesidir.
/// </summary> /// </summary>
public int Y { get; set; } public int Y { get; set; }
// allowDragging
// arrowLength
// arrowWidth
// axis
// component
// customizeTooltip
// data
// opacity
// render for react
// shadow {}
// template
// textOverflow
// tooltipComponent for react
// tooltipRender
// tooltipTemplate
} }

View file

@ -74,48 +74,4 @@ public class ChartArgumentAxisDto
/// <summary> Eksen üzerinde yalnızca iş günlerini bırakır: çalışma haftası günleri artı tek iş günleri eksi tatiller. Yalnızca eksenin argümanTürü "tarihsaat" ise geçerlidir. /// <summary> Eksen üzerinde yalnızca iş günlerini bırakır: çalışma haftası günleri artı tek iş günleri eksi tatiller. Yalnızca eksenin argümanTürü "tarihsaat" ise geçerlidir.
/// </summary> /// </summary>
public bool WorkdaysOnly { get; set; } public bool WorkdaysOnly { get; set; }
// ihtiyaç olunca açılacak proptert ler
// aggregateByCategory
// aggregatedPointsPosition
// aggregationGroupWidth
// allowDecimals
// axisDivisionFactor // İki komşu ana çizgi arasındaki minimum mesafeyi piksel cinsinden belirtir. Yalnızca "sürekli" ve "logaritmik" tiplerin eksenleri için geçerlidir.
// breaks[]
// breakStyle
// categories
// color
// constantLines[]
// constantLineStyle
// customPosition
// customPositionAxis
// holidays
// inverted
// linearThreshold
// logarithmBase
// maxValueMargin
// minorGrid
// minorTick
// minorTickCount
// minorTickInterval
// minValueMargin
// minVisualRangeLength
// offset
// opecity
// placeholderSize
// singleWorkdays
// strips[]
// stripStyle
// tick
// tickInterval
// type
// valueMarginsEnabled
// visualRange
// visualRangeUpdateMode
// wholeRange
// workWeek
} }

View file

@ -16,7 +16,7 @@ public class ChartBorderDto
public string Color { get; set; } = "#d3d3d3"; public string Color { get; set; } = "#d3d3d3";
/// <summary> Legend in tüm köşelerini yuvarlatır. /// <summary> Legend in tüm köşelerini yuvarlatır.
/// </summary> /// </summary>
[Range(0, 100, ErrorMessage = "Value for {0} must be between {1} and {2}.")] [Range(0, 100, ErrorMessage = "App.Validation.RangeBetween")]
public int CornerRadius { get; set; } = 0; public int CornerRadius { get; set; } = 0;
/// <summary> Açıklamanın kenarlığı için bir tire stili ayarlar /// <summary> Açıklamanın kenarlığı için bir tire stili ayarlar
/// Accepted Values: 'dash' | 'dot' | 'longDash' | 'solid' /// Accepted Values: 'dash' | 'dot' | 'longDash' | 'solid'
@ -31,7 +31,6 @@ public class ChartBorderDto
///// <summary> Açıklamanın kenarlığının saydamlığını belirtir ///// <summary> Açıklamanın kenarlığının saydamlığını belirtir
///// Bu özellik, 0 ile 1 arasında bir değer kabul eder; burada 0, kenarlığı tamamen saydam, 1 ise opak yapar. ///// Bu özellik, 0 ile 1 arasında bir değer kabul eder; burada 0, kenarlığı tamamen saydam, 1 ise opak yapar.
///// </summary> ///// </summary>
//[Range(0.1, 1, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
//public double Opacity { get; set; } = 0.5; //public double Opacity { get; set; } = 0.5;
public bool Visible { get; set; } public bool Visible { get; set; }
/// <summary> Açıklamanın kenarlığının genişliğini piksel cinsinden belirtir /// <summary> Açıklamanın kenarlığının genişliğini piksel cinsinden belirtir

View file

@ -81,21 +81,4 @@ public class ChartCommonAnnotationDto
/// <summary> Ek açıklamanın merkezini belirli bir piksel koordinatına konumlandırmak için x ile birlikte kullanılır. (0, 0), UI bileşeninin sol üst köşesidir. /// <summary> Ek açıklamanın merkezini belirli bir piksel koordinatına konumlandırmak için x ile birlikte kullanılır. (0, 0), UI bileşeninin sol üst köşesidir.
/// </summary> /// </summary>
public int Y { get; set; } public int Y { get; set; }
// allowDragging
// arrowLength
// arrowWidth
// axis
// component
// customizeTooltip
// data
// opacity
// render for react
// shadow {}
// template
// textOverflow
// tooltipComponent for react
// tooltipRender
// tooltipTemplate
} }

View file

@ -21,25 +21,4 @@ public class ChartCommonAxisDto
/// <summary> Eksen çizgisinin genişliğini piksel cinsinden belirtir. /// <summary> Eksen çizgisinin genişliğini piksel cinsinden belirtir.
/// </summary> /// </summary>
public int Width { get; set; } = 1; public int Width { get; set; } = 1;
// ihtiyaç olunca açılacak proptert ler
// aggregatedPointsPosition
// allowDecimals
// breakStyle {}
// color
// constantLineStyle {}
// discreteAxisDivisionMode
// endOnTick
// inverted
// label {}
// maxValueMargin
// minorGrid {}
// minorTick {}
// minValueMargin
// opacity
// placeholderSize
// stripStyle {}
// tick {}
// valueMarginsEnabled
} }

View file

@ -34,27 +34,4 @@ public class ChartCommonDto
/// Accepted Values: 'generic.dark' | 'generic.light' | 'generic.contrast' | 'generic.carmine' | 'generic.darkmoon' | 'generic.darkviolet' | 'generic.greenmist' | 'generic.softblue' | 'material.blue.light' | 'material.lime.light' | 'material.orange.light' | 'material.purple.light' | 'material.teal.light' /// Accepted Values: 'generic.dark' | 'generic.light' | 'generic.contrast' | 'generic.carmine' | 'generic.darkmoon' | 'generic.darkviolet' | 'generic.greenmist' | 'generic.softblue' | 'material.blue.light' | 'material.lime.light' | 'material.orange.light' | 'material.purple.light' | 'material.teal.light'
/// </summary> /// </summary>
public string Theme { get; set; } = "generic.light"; public string Theme { get; set; } = "generic.light";
// diğer eklenebilecek property ler
// autoHidePointMarkers
// barGroupPadding
// barGroupWidth
// customizeAnnotation
// customizeLabel
// customizePoint
// elementAttr
// maxBubbleSize
// minBubbleSize
// negativesAsZeroes
// pathModified
// pointSelectionMode
// redrawOnResize
// resizePanesOnZoom
// resolveLabelOverlapping
// rotated
// rtlEnabled
// seriesSelectionMode
// stickyHovering
// synchronizeMultiAxes
} }

View file

@ -99,49 +99,5 @@ public class ChartCommonSeriesSettingsDto
/// </summary> /// </summary>
public int Width { get; set; } = 2; public int Width { get; set; } = 2;
// aggregation {}
// area {}
// bar {}
// border {}
// bubble {}
// candlestick {}
// closeValueField // for financial series
// fullstackedarea {}
// fullstackedbar {}
// fullstackedline {}
// fullstackedspline {}
// fullstackedsplinearea {}
// highValueField
// hoverMode
// hoverStyle
// innerColor
// label {}
// line {}
// lowValueField
// maxLabelCount
// minBarSize
// opacity
// openValueField
// point {}
// rangearea {}
// rangebar {}
// reduction {} // financial seriler için indirgeme özelliklerini belirtir.
// scatter {}
// slectionStyle // Bir kullanıcı seçtiğinde seri tarafından benimsenen görünümü yapılandırır.
// sizeField
// spline {}
// splinearea {}
// stack {}
// stackedarea {}
// stackedbar {}
// stackedline {}
// stackedspline{}
// stackedsplinearea {}
// steparea {}
// stepline {}
// stock {}
// tagField
// valueErrorBar
public ChartLabelDto Label { get; set; } public ChartLabelDto Label { get; set; }
} }

View file

@ -40,33 +40,4 @@ public class ChartLegendDto
/// <summary> Göstergenin görünürlüğünü belirtir /// <summary> Göstergenin görünürlüğünü belirtir
/// </summary> /// </summary>
public bool Visible { get; set; } = true; public bool Visible { get; set; } = true;
////İhtiyaç olması durumunda açılacak diğer property ler
///// <summary>horizontalAlignment ile birlikte göstergenin konumunu belirtir
///// Accepted Values: 'bottom' | 'top'
///// </summary>
//public string VerticalAlignment { get; set; } = "top";
//public int RowItemSpacing { get; set; } = 8;
//public int PaddingTopBottom { get; set; } = 10;
//public int PaddingLeftRight { get; set; } = 10;
//public string MarkerTemplate { get; set; }
//public string MarkerSize { get; set; }
//public string MarkerRender { get; set; }
//public string MarkerComponent { get; set; }
//public string Margin { get; set; }
//public string ItemTextPosition { get; set; }
//public string ItemsAlignment { get; set; }
//public string HoverMode { get; set; }
//public string HorizontalAlignment { get; set; }
//public string CustomizeText { get; set; }
//public string CustomizeItems { get; set; }
//public string CustomizeHint { get; set; }
///// <summary> Öğe sütunları arasında piksel cinsinden boş bir alan belirtir
///// </summary>
//public int ColumnItemSpacing { get; set; } = 20;
} }

View file

@ -101,25 +101,6 @@ public class ChartSeriesDto
/// </summary> /// </summary>
public int Width { get; set; } = 2; public int Width { get; set; } = 2;
// hoverStyle
// hoverMode
// highValueField // for financial series
// closeValueField // for financial series
// border Dto
// innerColor
// label Dto
// lowValueField
// maxLabelCount
// minBarSize
// openValueField
// point Dto
// reduction // financial seriler için indirgeme özelliklerini belirtir.
// slectionStyle // Bir kullanıcı seçtiğinde seri tarafından benimsenen görünümü yapılandırır.
// stack
// tag
// tagField
// valueErrorBar
public ChartLabelDto Label { get; set; } public ChartLabelDto Label { get; set; }
/// <summary> Değer özetleme işlevini belirtir. /// <summary> Değer özetleme işlevini belirtir.

View file

@ -55,45 +55,4 @@ public class ChartValueAxisDto
public string Type { get; set; } public string Type { get; set; }
public bool AutoBreaksEnabled { get; set; } = true; public bool AutoBreaksEnabled { get; set; } = true;
public int MaxAutoBreakCount { get; set; } = 2; public int MaxAutoBreakCount { get; set; } = 2;
// ihtiyaç olunca açılacak property ler
// aggregatedPointsPosition
// allowDecimals
// axisDivisionFactor
// categories
// color
// constantLines[]
// constantLineStyle
// customPosition
// discreteAxisDivisionMode
// endOnTick
// inverted
// label
// linearThreshold
// logarithmBase
// maxValueMargin
// minorGrid
// minorTick
// minorTickCount
// minorTickInterval
// minValueMargin
// minVisualRangeLength
// multipleAxesSpacing
// offset
// opacity
// pane
// placeholderSize
// showZero
// strips[]
// stripStyle
// synchronizedValue
// tick
// tickInterval
// valueMarginsEnabled
// visualRange
// visualRangeUpdateMode
// wholeRange
} }

View file

@ -10,11 +10,6 @@ namespace Sozsoft.Platform.ListForms;
public class ColumnFormatDto : AuditedEntityDto<Guid> public class ColumnFormatDto : AuditedEntityDto<Guid>
{ {
private static readonly JsonSerializerOptions EditorOptionsSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
public ColumnFormatDto() { } public ColumnFormatDto() { }
public string FieldName { get; set; } public string FieldName { get; set; }
@ -282,7 +277,7 @@ public class ColumnFormatDto : AuditedEntityDto<Guid>
try try
{ {
return JsonSerializer.Deserialize<T>(EditorOptions, EditorOptionsSerializerOptions); return JsonSerializer.Deserialize<T>(EditorOptions, PlatformJsonOptions.CaseInsensitive);
} }
catch (JsonException) catch (JsonException)
{ {

View file

@ -1,10 +0,0 @@
namespace Sozsoft.Platform.ListForms.ImportManager;
public class ColumnMappingDto
{
public string SourceColumn { get; set; } // CSV/Excel'deki sütun adı
public string TargetField { get; set; } // Hedef Entity içindeki alan
public bool IsRequired { get; set; }
public string DataType { get; set; } // "string", "date", "number", "boolean"
}

View file

@ -1,8 +0,0 @@
public class ImportValidationErrorDto
{
public int Row { get; set; }
public string Column { get; set; }
public string Message { get; set; }
public string Value { get; set; }
public string Severity { get; set; } // örn: "error", "warning"
}

View file

@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms.Select;
public class WidgetDto public class WidgetDto
{ {
public int ColGap { get; set; } public int ColGap { get; set; }

View file

@ -55,8 +55,6 @@ public class WizardScreenOverlay
[nameof(WizardColumnItemInputDto.DbSourceType)] = nameof(ColumnFormatEditDto.SourceDbType), [nameof(WizardColumnItemInputDto.DbSourceType)] = nameof(ColumnFormatEditDto.SourceDbType),
}; };
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
/// <summary> /// <summary>
/// Katmani wizard'in urettigi ListForm'a uygular. <paramref name="baseline"/> ayni donusumun /// Katmani wizard'in urettigi ListForm'a uygular. <paramref name="baseline"/> ayni donusumun
/// <see cref="Baseline"/> wizard'i icin ciktisidir. /// <see cref="Baseline"/> wizard'i icin ciktisidir.
@ -145,7 +143,7 @@ public class WizardScreenOverlay
target[key] = value; target[key] = value;
} }
return target.Deserialize<T>(ReadOptions); return target.Deserialize<T>(PlatformJsonOptions.CaseInsensitive);
} }
/// <summary> /// <summary>

View file

@ -187,8 +187,6 @@ public class WizardSeedFileDto
WriteIndented = true, WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
}; };
private static readonly JsonSerializerOptions SeedReadOptions = new() { PropertyNameCaseInsensitive = true };
/// <summary> /// <summary>
/// Dosyaya yazilacak JSON'u uretir: once ortama ozgu alanlar temizlenir, sonra wizard /// Dosyaya yazilacak JSON'u uretir: once ortama ozgu alanlar temizlenir, sonra wizard
/// bolumunden ekranin sozlesmesinde zaten bulunan alanlar cikarilir. Wizard bolumunde /// bolumunden ekranin sozlesmesinde zaten bulunan alanlar cikarilir. Wizard bolumunde
@ -287,7 +285,7 @@ public class WizardSeedFileDto
if (node?[nameof(Wizard)] is JsonObject wizardNode) if (node?[nameof(Wizard)] is JsonObject wizardNode)
FlattenWizard(wizardNode); FlattenWizard(wizardNode);
return node.Deserialize<WizardSeedFileDto>(SeedReadOptions)?.Rehydrate(); return node.Deserialize<WizardSeedFileDto>(PlatformJsonOptions.CaseInsensitive)?.Rehydrate();
} }
/// <summary> /// <summary>
@ -326,7 +324,7 @@ public class WizardSeedFileDto
return null; return null;
FlattenWizard(wizardNode); FlattenWizard(wizardNode);
var wizard = wizardNode.Deserialize<ListFormWizardDto>(SeedReadOptions); var wizard = wizardNode.Deserialize<ListFormWizardDto>(PlatformJsonOptions.CaseInsensitive);
if (wizard == null) if (wizard == null)
return null; return null;
@ -344,7 +342,7 @@ public class WizardSeedFileDto
[nameof(Wizard)] = wizardNode.DeepClone(), [nameof(Wizard)] = wizardNode.DeepClone(),
[nameof(ListForm)] = listForm?.DeepClone() ?? new JsonObject(), [nameof(ListForm)] = listForm?.DeepClone() ?? new JsonObject(),
[nameof(Fields)] = fields?.DeepClone() ?? new JsonArray(), [nameof(Fields)] = fields?.DeepClone() ?? new JsonArray(),
}.Deserialize<WizardSeedFileDto>(SeedReadOptions).Rehydrate().Wizard; }.Deserialize<WizardSeedFileDto>(PlatformJsonOptions.CaseInsensitive).Rehydrate().Wizard;
if (listForm != null) if (listForm != null)
FillMissing(wizard, sections, wizardNode, ListFormOwnedWizardKeys); FillMissing(wizard, sections, wizardNode, ListFormOwnedWizardKeys);

View file

@ -1,5 +1,7 @@
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Entities;
public class NoteListRequestDto : PagedAndSortedResultRequestDto public class NoteListRequestDto : PagedAndSortedResultRequestDto
{ {
public string EntityName { get; set; } public string EntityName { get; set; }

View file

@ -1,9 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Public;
public class CountryGroupDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
}

View file

@ -1,10 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Skills;
public class SkillDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public string TypeName { get; set; }
}

View file

@ -1,12 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Skills;
public class SkillLevelDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public int Progress { get; set; }
public bool IsDefault { get; set; }
public string TypeName { get; set; }
}

View file

@ -25,14 +25,6 @@ public partial class AiBotToAiBotDtoMapper : TwoWayMapperBase<AiBot, AiBotDto>
[MapperIgnoreTarget(nameof(AiBotDto.LastModifierId))] [MapperIgnoreTarget(nameof(AiBotDto.LastModifierId))]
public override partial void Map(AiBot source, AiBotDto destination); public override partial void Map(AiBot source, AiBotDto destination);
public override void BeforeMap(AiBot source)
{
}
public override void AfterMap(AiBot source, AiBotDto destination)
{
}
[MapperIgnoreTarget(nameof(AiBot.IsActive))] [MapperIgnoreTarget(nameof(AiBot.IsActive))]
public override partial AiBot ReverseMap(AiBotDto destination); public override partial AiBot ReverseMap(AiBotDto destination);

View file

@ -12,14 +12,6 @@ public partial class AuditLogToAuditLogDtoMapper : MapperBase<AuditLog, AuditLog
[MapperIgnoreTarget(nameof(AuditLogDto.EntityChangeCount))] [MapperIgnoreTarget(nameof(AuditLogDto.EntityChangeCount))]
public override partial void Map(AuditLog source, AuditLogDto destination); public override partial void Map(AuditLog source, AuditLogDto destination);
public override void BeforeMap(AuditLog source)
{
}
public override void AfterMap(AuditLog source, AuditLogDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -28,14 +20,6 @@ public partial class AuditLogActionToAuditLogActionDtoMapper : MapperBase<AuditL
public override partial AuditLogActionDto Map(AuditLogAction source); public override partial AuditLogActionDto Map(AuditLogAction source);
public override partial void Map(AuditLogAction source, AuditLogActionDto destination); public override partial void Map(AuditLogAction source, AuditLogActionDto destination);
public override void BeforeMap(AuditLogAction source)
{
}
public override void AfterMap(AuditLogAction source, AuditLogActionDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -44,14 +28,6 @@ public partial class EntityChangeToEntityChangeDtoMapper : MapperBase<EntityChan
public override partial EntityChangeDto Map(EntityChange source); public override partial EntityChangeDto Map(EntityChange source);
public override partial void Map(EntityChange source, EntityChangeDto destination); public override partial void Map(EntityChange source, EntityChangeDto destination);
public override void BeforeMap(EntityChange source)
{
}
public override void AfterMap(EntityChange source, EntityChangeDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -60,12 +36,4 @@ public partial class EntityPropertyChangeToEntityPropertyChangeDtoMapper : Mappe
public override partial EntityPropertyChangeDto Map(EntityPropertyChange source); public override partial EntityPropertyChangeDto Map(EntityPropertyChange source);
public override partial void Map(EntityPropertyChange source, EntityPropertyChangeDto destination); public override partial void Map(EntityPropertyChange source, EntityPropertyChangeDto destination);
public override void BeforeMap(EntityPropertyChange source)
{
}
public override void AfterMap(EntityPropertyChange source, EntityPropertyChangeDto destination)
{
}
} }

View file

@ -2,12 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Hangfire; using Hangfire;
using Hangfire.Storage; using Hangfire.Storage;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sozsoft.MailQueue.Domain.Shared;
using Sozsoft.Platform.BackgroundWorkers; using Sozsoft.Platform.BackgroundWorkers;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
@ -22,20 +20,17 @@ public class BackgroundWorkerInitializer : ITransientDependency
{ {
private readonly IRepository<BackgroundWorker, Guid> _repo; private readonly IRepository<BackgroundWorker, Guid> _repo;
private readonly ITenantRepository _tenantRepository; private readonly ITenantRepository _tenantRepository;
private readonly PlatformTemplateManager _templateManager;
private readonly ILogger<BackgroundWorkerInitializer> _logger; private readonly ILogger<BackgroundWorkerInitializer> _logger;
private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkManager _unitOfWorkManager;
public BackgroundWorkerInitializer( public BackgroundWorkerInitializer(
IRepository<BackgroundWorker, Guid> repo, IRepository<BackgroundWorker, Guid> repo,
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
PlatformTemplateManager templateManager,
ILogger<BackgroundWorkerInitializer> logger, ILogger<BackgroundWorkerInitializer> logger,
IUnitOfWorkManager unitOfWorkManager) IUnitOfWorkManager unitOfWorkManager)
{ {
_repo = repo; _repo = repo;
_tenantRepository = tenantRepository; _tenantRepository = tenantRepository;
_templateManager = templateManager;
_logger = logger; _logger = logger;
_unitOfWorkManager = unitOfWorkManager; _unitOfWorkManager = unitOfWorkManager;
} }
@ -93,19 +88,8 @@ public class BackgroundWorkerInitializer : ITransientDependency
}); });
} }
foreach (var worker in workers) // Mail şablonları burada kaydedilmez: PlatformTemplateDynamicStore şablonu istendiği anda
{ // işin Options.MailTemplate alanından okur.
if (worker.WorkerType == WorkerTypeEnum.MailQueueWorker && !worker.Options.IsNullOrWhiteSpace())
{
var options = JsonSerializer.Deserialize<MailQueueWorkerOptions>(worker.Options);
if (options != null && !options.MailTemplate.IsNullOrEmpty())
{
await _templateManager.AddOrUpdateTemplateAsync(worker.Name, options.MailTemplate);
}
}
}
_logger.LogInformation( _logger.LogInformation(
"{WorkerCount} active background workers loaded into Hangfire.", "{WorkerCount} active background workers loaded into Hangfire.",
workers.Count); workers.Count);

View file

@ -80,7 +80,7 @@ public class BlogAppService : PlatformAppService, IBlogAppService
post.ContentTr = input.ContentTr; post.ContentTr = input.ContentTr;
post.ContentEn = input.ContentEn; post.ContentEn = input.ContentEn;
if (input.IsPublished) post.Publish(); else post.Unpublish(); if (input.IsPublished) post.Publish(Clock.Now); else post.Unpublish();
if (post.CategoryId != input.CategoryId) if (post.CategoryId != input.CategoryId)
{ {
@ -143,7 +143,7 @@ public class BlogAppService : PlatformAppService, IBlogAppService
throw new Volo.Abp.Authorization.AbpAuthorizationException(); throw new Volo.Abp.Authorization.AbpAuthorizationException();
} }
post.Publish(); post.Publish(Clock.Now);
await _postRepository.UpdateAsync(post); await _postRepository.UpdateAsync(post);
return await GetPostAsync(id); return await GetPostAsync(id);

View file

@ -1,11 +1,11 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform;
using Sozsoft.Platform.Branchs;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Branchs;
public class BranchAppService : PlatformAppService public class BranchAppService : PlatformAppService
{ {
private readonly IRepository<Branch, Guid> _branchRepository; private readonly IRepository<Branch, Guid> _branchRepository;
@ -26,7 +26,7 @@ public class BranchAppService : PlatformAppService
// Branch kiracı filtresi taşımaz; kiracı kullanıcısı yalnızca kendi şubesini seed edebilir, host hepsini. // Branch kiracı filtresi taşımaz; kiracı kullanıcısı yalnızca kendi şubesini seed edebilir, host hepsini.
var branch = await _branchRepository.FirstOrDefaultAsync(x => x.Id == branchId); var branch = await _branchRepository.FirstOrDefaultAsync(x => x.Id == branchId);
if (branch == null || (CurrentTenant.Id.HasValue && branch.TenantId != CurrentTenant.Id)) if (branch == null || (CurrentTenant.Id.HasValue && branch.TenantId != CurrentTenant.Id))
return new BranchSeedResultDto { Success = false, Message = "Branch not found." }; return new BranchSeedResultDto { Success = false, Message = L["App.BranchSeed.NotFound"] };
using (CurrentTenant.Change(branch.TenantId)) using (CurrentTenant.Change(branch.TenantId))
{ {

View file

@ -9,14 +9,6 @@ public partial class BranchSeedResultToBranchSeedResultDtoMapper : MapperBase<Br
public override partial BranchSeedResultDto Map(BranchSeedResult source); public override partial BranchSeedResultDto Map(BranchSeedResult source);
public override partial void Map(BranchSeedResult source, BranchSeedResultDto destination); public override partial void Map(BranchSeedResult source, BranchSeedResultDto destination);
public override void BeforeMap(BranchSeedResult source)
{
}
public override void AfterMap(BranchSeedResult source, BranchSeedResultDto destination)
{
}
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
@ -25,12 +17,4 @@ public partial class BranchSeedDetailToSeedDetailDtoMapper : MapperBase<BranchSe
public override partial SeedDetailDto Map(BranchSeedDetail source); public override partial SeedDetailDto Map(BranchSeedDetail source);
public override partial void Map(BranchSeedDetail source, SeedDetailDto destination); public override partial void Map(BranchSeedDetail source, SeedDetailDto destination);
public override void BeforeMap(BranchSeedDetail source)
{
}
public override void AfterMap(BranchSeedDetail source, SeedDetailDto destination)
{
}
} }

View file

@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.OrganizationUnits;
using Volo.Abp.Users;
namespace Sozsoft.Platform.Dashboard;
[Authorize()]
public class DashboardAppService : PlatformAppService
{
public ISpRepository SpRepository { get; }
public IDynamicDataManager DynamicDataManager { get; }
public IPlatformOuRepository OuRepository { get; }
public DashboardAppService(
ISpRepository spRepository,
IDynamicDataManager dynamicDataManager,
IPlatformOuRepository ouRepository)
{
SpRepository = spRepository;
DynamicDataManager = dynamicDataManager;
OuRepository = ouRepository;
}
public async Task<List<DashboardWidgetsDto>> GetWidgetsAsync(DashboardWidgetsRequestDto requestDto)
{
var ids = await OuRepository.GetOrganizationUnitIdsWithChildren(CurrentUser.GetId());
var results = await SpRepository.CallSpAsync("PDashboard_Widgets", "KIM",
new Dictionary<string, object>() {
{ "tarih1", requestDto.Tarih1 },
{ "tarih2", requestDto.Tarih2 },
{ "ids", ids.IsNullOrEmpty() ? Guid.Empty : ids.JoinAsString("','")}
}
);
var dtoResults = new List<DashboardWidgetsDto>();
foreach (var item in results)
{
var dto = new DashboardWidgetsDto
{
Id = Convert.ToInt32(item["id"]),
Label = item["label"]?.ToString(),
ValuePrefix = item["valueprefix"]?.ToString(),
Date = Convert.ToDateTime(item["date"]),
DataValue = Convert.ToSingle(item["datavalue"]),
DataGrowShrink = Convert.ToSingle(item["datagrowshrink"])
};
dtoResults.Add(dto);
}
return dtoResults;
}
}

View file

@ -11,14 +11,6 @@ public partial class DataSourceToDataSourceDtoMapper : TwoWayMapperBase<DataSour
public override partial void Map(DataSource source, DataSourceDto destination); public override partial void Map(DataSource source, DataSourceDto destination);
public override void BeforeMap(DataSource source)
{
}
public override void AfterMap(DataSource source, DataSourceDto destination)
{
}
public override partial DataSource ReverseMap(DataSourceDto destination); public override partial DataSource ReverseMap(DataSourceDto destination);
public override partial void ReverseMap(DataSourceDto destination, DataSource source); public override partial void ReverseMap(DataSourceDto destination, DataSource source);

View file

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Domain.DeveloperKit; using Sozsoft.Platform.Domain.DeveloperKit;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Localization;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -31,6 +32,7 @@ public class CrudEndpointAppService : ApplicationService
{ {
_endpointRepository = endpointRepository; _endpointRepository = endpointRepository;
_dynamicManager = dynamicManager; _dynamicManager = dynamicManager;
LocalizationResource = typeof(PlatformResource);
} }
/// <summary> /// <summary>
@ -102,7 +104,7 @@ public class CrudEndpointAppService : ApplicationService
x.IsActive); x.IsActive);
if (!exists) if (!exists)
throw new UserFriendlyException($"No active endpoint defined for {entityName} {operation}"); throw new UserFriendlyException(L["App.DeveloperKitEndpoint.NoActiveEndpoint", entityName, operation]);
} }
} }

View file

@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Queries;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Tracing; using Volo.Abp.Tracing;
using Volo.Abp.Uow; using Volo.Abp.Uow;
@ -104,7 +105,6 @@ public class CustomEndpointAppService : PlatformAppService
return new UnauthorizedResult(); return new UnauthorizedResult();
} }
Dictionary<string, object?> param = []; Dictionary<string, object?> param = [];
// Parametreler: // Parametreler:
// 1- Statik // 1- Statik
@ -277,81 +277,3 @@ public class CustomEndpointAppService : PlatformAppService
return values; return values;
} }
} }
//TODO: Custom Endpoint rol, permission seed
/*
Token İsteği Örnek:
POST /connect/token HTTP/1.1
Host: localhost:44344
Content-Type: application/x-www-form-urlencoded
username=system%40sozsoft.com
&password=...
&grant_type=password
&client_id=Platform_PublicApi
&scope=offline_access%20Platform
Custom Endpoint Seed:
SELECT * FROM PLanguage WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName
INSERT INTO [dbo].[Orders]
([CustomerName]
,[ProductName]
,[OrderDate]
,[Quantity])
OUTPUT Inserted.*
VALUES
(@CustomerName, @ProductName, @OrderDate, @Quantity)
SELECT * FROM Orders WHERE Id = SCOPE_IDENTITY()
[{ "Type": "S", "Name": "CultureName", "DefaultValue": "ar" }]
[{ "Type": "Q", "Name": "CultureName", "DefaultValue": "ar" }]
[
{ "Type": "P", "Name": "IsEnabled", "DefaultValue": "en", "Path": "/yxcdfn/8/:IsEnabled/:CultureName/" },
{ "Type": "P", "Name": "CultureName", "DefaultValue": "en", "Path": "/yxcdfn/8/:IsEnabled/:CultureName/" }
]
[
{ "Type": "B", "Name": "CustomerName", "DefaultValue": "" },
{ "Type": "B", "Name": "ProductName", "DefaultValue": "" },
{ "Type": "B", "Name": "OrderDate", "DefaultValue": "@NOW" },
{ "Type": "B", "Name": "Quantity", "DefaultValue": "" }
]
Guid? TenantId
string Name
string Description
string Url -> https://api.sozsoft.com/api/app/dinamik/yxfgu
string Method -> GET
string Params = [
{ Type: 'Static', Name: 'StartDate', DefaultValue: '234' },
{ Type: 'Query', Name: 'StartDate', DefaultValue: '', IsRequired: false },
{ Type: 'Path', Name: 'FaturaId', DefaultValue: '', Path: '/yxfgu/fatura/:FaturaId/kalem/357' },
{ Type: 'Path', Name: 'KalemId', DefaultValue: '', Path: '/yxfgu/fatura/xxx/kalem/:KalemId' },
{ Type: 'Body', Name: 'StartDate', DefaultValue: '' },
]
string Sql -> SELECT * FROM VSatislar WHERE MusteriId = @MusteriId AND StartDate >= @StartDate
string Permissions = [
{ ResourceType: 'User', ResourceId: 'system' },
{ ResourceType: 'User', ResourceId: 'sedat' },
]
Query Parameter
URL: https://api.sozsoft.com/api/app/dinamik/yxfgu
Method: GET
Parameters: ?StartDate=2024-12-31&EndDate=2025-12-31
Path Parameter
URL: https://api.sozsoft.com/api/app/dinamik/yxfgu/fatura/467/kalem/357
Method: GET
Parameters: FaturaId=467
Body Parameter
URL: https://api.sozsoft.com/api/app/dinamik/yxfgu?UrunId=5
Method: POST
Parameters: ?StartDate=2024-12-31&EndDate=2025-12-31
Body: { Tutar: 2000, Tarih: '2024-12-31' }
*/

Some files were not shown because too many files have changed in this diff Show more