diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 97e94125..13e68c43 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -70,7 +70,7 @@ ama hiçbirini görmeden bırakma; kullanıcı "şunu da yapabilir miydik?" dedi | 16 | **Gösterge** | Üstte KPI kartları, grafik | §5.3 · §8 | | 17 | **Süreç** | Onay akışı, bilgilendirme, durum alanları | `lowcode` §4.7 | | 18 | **Otomasyon** | Zamanlanmış iş, zamanlanmış e-posta, bildirim kuralı | `lowcode` §9.10–§9.11 · §11 | -| 19 | **Çıktı** | Hazır grid raporu ya da rapor şablonu | `lowcode` §9.12 | +| 19 | **Çıktı** | Hazır grid raporu ya da `Raporlar` kategorisinde `DynamicReport` şablonu (kayıt alanları `{{AlanAdi}}`) | `lowcode` §9.12 | | 20 | **Yetki** | Ekran, alan, komut ve buton yetkileri; menü görünürlüğü | §5.1 · `lowcode` §4.2 | | 21 | **Tenant/şube/satır kısıtı** | `IsTenant`, `IsBranch`, `IsOrganizationUnit`, sorgularda kırılım, rol/kullanıcıya `ServerWhere` | `lowcode` §4.2 · §2.5 | | 22 | **Dil** | Menü, başlık, alan, buton, mesaj metinleri EN + TR | `lowcode` §4.2 | @@ -1320,7 +1320,7 @@ ekranları ABP tablolarıdır, Licence/Dev Request host tablolarını okur. | **Identity › Organization Units** | `Abp.Identity.OrganizationUnits` · `/admin/ous` | ABP OU tabloları | Birim ağacı. Ekranı `IsOrganizationUnit = true` yaparsan veri bu ağaca göre süzülür; bildirim alıcısı da OU olabilir. | | **Identity › Audit Logs** | `App.IdentityManagement.AuditLogs` | `AbpAuditLogs` | İstek denetim kaydı + `AuditLogDetail` diyaloğu. "Kim ne zaman ne yaptı" isteği yeni tablo gerektirmez. | | **Identity › Sessions** | `App.IdentityManagement.Sessions` | `AbpSessions` | Açık oturumlar; `UiEvalService.ApiKickUser` ile oturum düşürme (`lowcode` §9.13). | -| **Reports › Categories / Templates** | `App.Reports.Categories` · `App.Reports.ReportTemplates` | `Adm_T_ReportCategory` · `Adm_T_ReportTemplate` | Kendi düzeni olan çıktılar (fatura, teklif); ekrana `CommandColumnJson` butonuyla bağlanır (`lowcode` §9.12). | +| **Reports › Categories / Templates** | `App.Reports.Categories` · `App.Reports.ReportTemplates` | `Adm_T_ReportCategory` · `Adm_T_ReportTemplate` | Kendi düzeni olan çıktılar (fatura, teklif, sertifika, taahhütname). `Raporlar` kategorisindeki şablon `DynamicReport` ile basılan HTML'dir; satırdan `/admin/reports/DynamicReport/view/{Id}/{ListFormCode}?recordId=@Id` ile açılır, `{{AlanAdi}}` kayıtla dolar. MCP: `list_report_templates`, `save_report_template`; ekrana wizard `CommandColumns` butonuyla bağlanır (`lowcode` §9.12). Seed bayrakları açık. | | **Videoroom** | `App.Videoroom` · `/admin/videoroom/dashboard` | `Adm_T_Videoroom` · `Adm_T_VideoroomParticipant` · `Adm_T_VideoroomAttandance` · `Adm_T_VideoroomChat` | Görüntülü oda, katılım ve sohbet kaydı. | | **Forum** | menü kodu `App.Forum` · yetki `App.ForumManagement.Publish` · `/admin/forum` | `Sas_T_Forum*` | Forumun kullanıcı tarafı. | @@ -1455,7 +1455,7 @@ alanları da doldurur (`lowcode` §0.9). | "bilgilendirme gitsin" | Administration › Bildirimler (+ MailQueue §11) | Tür + kural (8 kanal); tablolu/ekli e-posta ise MailQueue | | "elden bildirim/duyuru gönderelim" | Administration › Bildirimler › Bildirimler (+ Şablonlar) | Ekleme formu = elle gönderim; hazır metin için şablon kaydı | | "onaya düşsün" | `WorkflowJson` + `ListFormWorkflow` (örnek: Sales Orders) | Onay düğümleri; `Inform` düğümü e-posta gönderir | -| "çıktısı alınsın / fatura basılsın" | Administration › Reports | Hazır `DynamicGrid` ya da `ReportTemplate` | +| "çıktısı alınsın / fatura / sertifika basılsın" | Administration › Reports | Hazır `DynamicGrid` ya da `Raporlar` kategorisinde `DynamicReport` şablonu (`save_report_template`) | | "dosya eklensin" | Saas › Developer Kit › Files | Dosya/görsel editörü; ayrı depolama kurma | | "fotoğraf çekilsin / barkod okutulsun / konum alınsın" | Alan editörü (§9.8 tablosu) · tasarımcıda `BarcodeScanField` | `Capture`, `@SCAN_BARCODE`, `BarcodeScanDialog`, `geoTo`; yeni editör yazma | | "barkod/QR basılsın" | Visual Designer › `BarcodeViewer` | `value`'yu `Form` kolonuna bağla | diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 34d1c72f..029a4e2b 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -506,7 +506,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz. | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz | | `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` | -| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | +| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) | | `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) | | `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | @@ -515,6 +515,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz. | `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) | | `verify_wizard` | DB Migrate ile kurulmuş ekranı veri yazmadan doğrular (migrate bekleyen dosyada `install` kontrolü başarısız döner, diğerleri yapılmaz): menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | İsteğe bağlı: kullanıcı migrate sonrası isterse. Talep döngüsünün (§0.11) adımı değildir. `passed` yalnızca engelleyici bir kontrol kalırsa `false` | | `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) | +| `list_report_templates` · `get_report_template` · `save_report_category` · `save_report_template` · `delete_report_template` | Rapor kategorisi ve `DynamicReport` şablonu okuma/yazma (ada göre upsert) | Yazma `App.Reports.*` ekranlarının C/U/D sözleşmesinden geçer, seed dosyasına yansır. `listFormCode` ile yer tutucuları doğrular ve satır butonunu döner (§9.12) | **Yetkiler.** MCP'nin iki yetkisi vardır: `.Invoke` (bağlantı + okuma) ve `.Write` (her değişiklik; kiracıda yalnızca satır yazma). İkisi de kapıdır, işlemin kendi uygulama yetkisi ayrıca @@ -808,7 +809,7 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma. | "Kaydederken/silerken ek SQL çalışsın (stok, log, durum)" | `*BeforeCommand` / `*AfterCommand` | 4.8 | | "Şu iş her gece / her 5 dakikada çalışsın" | `BackgroundWorker` kaydı + stored procedure | 9.10 | | "Şu olayda bildirim gitsin" | Notification Type + Rule | 9.11 | -| "Fatura/teklif/iş emri çıktısı" | Rapor şablonu (ya da hazır `DynamicGrid`) | 9.12 | +| "Fatura/teklif/iş emri/sertifika çıktısı" | `Raporlar` kategorisinde `DynamicReport` şablonu + satır butonu (ya da hazır `DynamicGrid`) | 9.12 | | "Ayar ekranından açılıp kapansın" | Setting Definition | 9.14 | | Hiçbiri yetmiyorsa | Dynamic Service → en son çare kod | `ai.instructions.md` §4 | @@ -1611,6 +1612,8 @@ cevaba yazılmaz. } ], "SubForms": [], + // Satır/toolbar butonları (§4.9) — örneğin rapor şablonu açan satır butonu (§9.12). + "CommandColumns": [], // List ekranı widget'la gelir (§4.6.1): kiracı tablosu → her kartta TenantId koşulu. "Widgets": [ { @@ -2120,7 +2123,11 @@ olarak da görelim" isteği yeni bir ekran değil, `Todo: true` + `TodoOptionDto Grid'in araç çubuğuna ve satır aksiyon sütununa **özel butonlar** eklenir. Süreç yönetiminin (onaya gönder, iptal et, belge üret, dış sisteme aktar) kod yazmadan yapıldığı yer burasıdır. -`ListForm.CommandColumnJson` bir `CommandColumnDto[]` taşır: +`ListForm.CommandColumnJson` bir `CommandColumnDto[]` taşır. Wizard cevabında (§4.4) ve MCP +`deploy_wizard` girdisinde aynı dizi **`CommandColumns`** alanıdır; deploy onu +`CommandColumnJson`'a yazar, seed dosyasında `ListForm` bölümünde durur ve düzenlemede korunur. +Wizard ekranında bu alanın adımı yoktur; buton ekranın detayındaki **Komutlar** sekmesinden ya da +`deploy_wizard` ile yazılır. ```jsonc [ @@ -4237,18 +4244,43 @@ Raporlama DevExpress XtraReports tabanlıdır ve iki katmanı vardır: | Katman | Ne | | --- | --- | | **Hazır (predefined) raporlar** | `DynamicGrid`, `DynamicTree`, `DynamicForm` — o anki ekranın sütunlarını, filtrelerini ve verisini alıp yazdırılabilir çıktı üretir | -| **Rapor şablonları** | `App.Reports.ReportTemplates` + `App.Reports.Categories` ekranlarındaki kayıtlar; tasarımcıda çizilir, adıyla açılır | +| **Rapor şablonları** | `App.Reports.ReportTemplates` + `App.Reports.Categories` ekranlarındaki kayıtlar. `Raporlar` kategorisindeki şablon `DynamicReport` ile basılan HTML'dir; Id'siyle açılır, MCP `save_report_template` ile eklenir | Route'lar: görüntüleyici `/admin/reports/{report}/view/{id?}/{listFormCode?}`, tasarımcı `/admin/reports/{report}/design/{id?}/{listFormCode?}`. - Her List ekranının araç çubuğundaki rapor aksiyonu `DynamicGrid` (TreeList'te `DynamicTree`) raporunu **o anki filtrelerle** açar; bunun için hiçbir tanım yapılmaz. -- Kendi düzeni istenen çıktı (fatura, teklif, iş emri) için `ReportTemplate` kaydı açılır ve - tasarımcıdan çizilir; ekrana `CommandColumnJson` butonu ile bağlanır - (`Url: "/admin/reports/{ReportAdi}/view/@Id"`). +- Kendi düzeni istenen kayıt çıktısı (sertifika, taahhütname, teklif, iş emri) **`Raporlar` + kategorisinde bir `DynamicReport` şablonudur**: `HtmlContent` A4'e basılan HTML'dir, ayrı bir + `.repx` çizilmez. Kayıtsız görüntüleme `/admin/reports/DynamicReport/view/{ŞablonId}` + (`Standard Taahhütname` gibi). Bir ekran satırından açılınca adres + `/admin/reports/DynamicReport/view/{ŞablonId}/{ListFormCode}?recordId=@{KeyFieldName}` olur ve + HTML'deki `{{AlanAdi}}` yer tutucuları o kaydın alanlarıyla doldurulur. Değer, form raporunun + bastığı metindir (lookup görünen metni, statik lookup karşılığı, tarih/sayı biçimi) ve HTML + olarak kodlanır. Kayıt ekranın okuma yetkisi ve kiracı filtresiyle okunur. `@@date` rapor + tarihidir. Ekranda alanı olmayan bir değer yer tutucu olamaz; gerekiyorsa ekrana gizli alan + olarak eklenir. +- Şablon kiracıda bulunamazsa host şablonu kullanılır: host şablonu ürünle gelen ortak tasarımdır, + basılan veri yine kiracının kendi ekranındandır. +- Şablon ekrana `CommandColumnJson` satır butonu ile bağlanır; koşullu buton `VisibleExpression` + ile yazılır. Örnek: sertifika yalnızca katılan katılımcıda görünür, + `((e)=> e.row.data.AttendanceStatus === 'Attended')`. +- **MCP ile:** `list_report_templates` → `save_report_template` (`category: "Raporlar"`, + `htmlContent`, `listFormCode`). `listFormCode` verilirse yer tutucular o ekranın alanlarına + göre doğrulanır ve cevapta `rowButton` döner. Buton ekranın wizard cevabına `CommandColumns` + elemanı olarak yazılır (§4.9) ve `deploy_wizard` ile gider. Ekran henüz migrate edilmediyse + `listFormCode` verilmez; buton adresi `viewUrl` + `/{ListFormCode}?recordId=@Id` ile yazılır. + Sıra: önce ekran(lar) `deploy_wizard`, sonra şablon `save_report_template`, sonra butonlu ekran + `deploy_wizard` (`replaceFileName`) — ya da şablon önce kaydedilip Id'si butona yazılır. Yeni + kategori (`save_report_category`) yalnızca istenince açılır. +- Rapor şablonu **eksik yetenek değildir**: sertifika/taahhütname/belge çıktısı isteyen bir talep + bu araçlarla üretilir; analizde "tasarımcıda çizilir" ya da "seed'e yazılmaz" gerekçesiyle + `Ready to Start` engellenmez. - Rapor şablonları ve kategorileri `data/App.Reports.{ReportTemplates,Categories}.json` ile - seed'lenir; seed bayrakları kapalıdır, tasarımcıda yapılan değişiklik dosyaya yansımaz (§9.14). + seed'lenir ve seed bayrakları açıktır: ekrandan ya da MCP'den yapılan kayıt dosyaya yansır + (§9.14). `DynamicReport/design` ile kaydedilen sayfa çerçevesi (`.repx`) sunucudaki + `PredefinedReports/Reports` klasörüne yazılır, seed'e girmez. ### 9.13 `UiEvalService` — buton ve script'ten çağrılabilen platform aksiyonları @@ -4275,7 +4307,7 @@ kullanılacağı** `lowcode-reference.instructions.md` §12'dedir. | "Belge numarası otomatik olsun" | Sequence (§9.9) | `/admin/list/App.Definitions.Sequence` | | "Şu iş her gece çalışsın" | Recurring Jobs (§9.10) | `/admin/list/App.BackgroundWorkers.RecurringJobs` | | "Şu olayda bildirim gitsin" | Notification Types + Rules (§9.11) | `/admin/list/App.Notifications.NotificationRules` | -| "Fatura/teklif çıktısı" | Report Templates (§9.12) | `/admin/list/App.Reports.ReportTemplates` | +| "Fatura/teklif/sertifika çıktısı" | Report Templates — `Raporlar` kategorisi, `DynamicReport` (§9.12); MCP `save_report_template` | `/admin/list/App.Reports.ReportTemplates` | | "Yeni bir veritabanı bağlantısı" | Data Source | `/admin/list/App.DataSource` | | "Uygulama ayarı (e-posta, limit, açık/kapalı)" | Setting Definitions + Ayarlar ekranı | `/admin/list/App.SettingDefinitions` · `/admin/settings` | | "Metin/çeviri değişsin" | Languages + Language Texts | `/admin/list/App.Languages.LanguageText` | @@ -4298,8 +4330,8 @@ kullanılacağı** `lowcode-reference.instructions.md` §12'dedir. > > | Durum | Ekranlar (örnek) | Prompt ile kayıt eklerken | > | --- | --- | --- | -> | Seed bayrakları açık — ekrandan yapılan kayıt dosyaya yansır | RecurringJobs, NotificationTemplates/Types/Rules, SettingDefinitions, AiBot, CustomEndpoints, DynamicServices, CustomComponents, McpServers, IntranetWidgets, `App.Menus.*`, `Abp.Identity.Permission*` | Satırı `data/{ListFormCode}.json`'a yaz | -> | `SeedFilePath` dolu, bayraklar kapalı — dosya migrate'te uygulanır, ekrandan yapılan değişiklik dosyaya **yansımaz** | Sequence, GlobalSearch, Currency, ContactTitle, Country/City/District, Uom, Hr Department/JobPosition/SkillType, ReportTemplates/Categories, WorkHour, Intranet, Blog, Forum, Orders tanımları | Satırı dosyaya yaz; ekrandan eklenen kayıt teslim sayılmaz | +> | Seed bayrakları açık — ekrandan yapılan kayıt dosyaya yansır | RecurringJobs, NotificationTemplates/Types/Rules, SettingDefinitions, AiBot, CustomEndpoints, DynamicServices, CustomComponents, McpServers, IntranetWidgets, ReportTemplates/Categories, `App.Menus.*`, `Abp.Identity.Permission*` | Satırı `data/{ListFormCode}.json`'a yaz (rapor şablonu için `save_report_template`) | +> | `SeedFilePath` dolu, bayraklar kapalı — dosya migrate'te uygulanır, ekrandan yapılan değişiklik dosyaya **yansımaz** | Sequence, GlobalSearch, Currency, ContactTitle, Country/City/District, Uom, Hr Department/JobPosition/SkillType, WorkHour, Intranet, Blog, Forum, Orders tanımları | Satırı dosyaya yaz; ekrandan eklenen kayıt teslim sayılmaz | > | `SeedFilePath` boş — yalnızca veritabanı | DataSource, Languages, Branches, Roles/Users/Tenants, AuditLogs, IpRestrictions, Notification (geçmiş), BackgroundWorkers.Jobs | `sql/execute/{Ad}.sql` altında **varsa güncelleyen/yoksa ekleyen** bir prosedür (§8.4) | > > Tam liste `ListFormSeeder_Saas.cs` / `ListFormSeeder_Administration.cs` / `ListFormSeeder_Hr.cs` içindeki `SeedFilePath` diff --git a/README.md b/README.md index 0f1115a4..5da81b25 100644 --- a/README.md +++ b/README.md @@ -1016,7 +1016,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer | Forum | `/admin/forum` | Forum kullanımı; yönetim `App.ForumManagement` altındaki ListForm ekranlarıyla yapılır. | | Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Sas_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. | | Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. | -| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. | +| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. `Raporlar` kategorisindeki şablon: `/admin/reports/DynamicReport/view/{ŞablonId}[/{ListFormCode}?recordId=...]` (§15). | | AI asistanı | `/admin/ai` | n8n webhook üzerinden sohbet arayüzü. | | Profil | `/admin/profile/*` | Genel bilgiler, şifre, bildirim ayarları. | @@ -1464,11 +1464,13 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. | | `append_dev_request_note` | Talebin `Analysis` kolonunun sonuna HTML ekler (üretim sonuç notu); `Description` talebin yazıldığı hâliyle kalır. İkisi de `update_row` ile yazılmaz — açıklama base64 görsel taşır. `.Write` + `App.DevRequests.Update` ister. | | `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. | +| `list_report_templates` / `get_report_template` / `save_report_category` / `save_report_template` / `delete_report_template` | Rapor kategorileri ve `DynamicReport` şablonları (`McpReportAppService`). Okuma depodan, ekranın okuma yetkisiyle yapılır. Yazma `App.Reports.Categories` / `App.Reports.ReportTemplates` ekranlarının C/U/D sözleşmesinden (`IListFormDataAppService`) geçer; seed senkronu aynen çalışır. Kayıt ada göre ekler/günceller. `listFormCode` verilirse HTML'deki `{{AlanAdi}}` yer tutucuları o ekranın alanlarına göre doğrulanır ve cevapta satır butonu (`rowButton`) döner (§15). | **Timesheets.** Talebin `Timesheets` listesini sunucu doldurur (`IMcpDevRequestTimesheetRecorder`): `get_dev_request` / `start_dev_request` talebi kullanıcının etkin talebi yapar (dağıtık önbellek, `McpDevRequestActivityCacheItem`, 12 saat); sonraki her MCP yazma işlemi (`create_table`, -`deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, satır araçları, +`deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, +`save_report_template`, `delete_report_template`, satır araçları, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks`) o talebe bir satır ekler; talebi açan `start_dev_request` yazmaz. Açıklama `App.Mcp.Timesheet.*` anahtarından Türkçe üretilir, süre önceki satırdan ya da talebin açıldığı andan ölçülür (en az 1 dakika, saat @@ -1525,7 +1527,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay | --- | --- | --- | --- | | `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.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `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`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `save_report_category`, `save_report_template`, `delete_report_template`, `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ı) @@ -1545,6 +1547,10 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay | `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` | | `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` | | `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` | +| `list_report_templates`, `get_report_template` | — | — | `App.Reports.ReportTemplates` | +| `save_report_category` | — | `.Write` | `App.Reports.Categories.Create` (yeni) / `.Update` | +| `save_report_template` | — | `.Write` | `App.Reports.ReportTemplates.Create` (yeni) / `.Update` | +| `delete_report_template` | — | `.Write` | `App.Reports.ReportTemplates.Delete` | | `start_dev_request` | — | `.Write` | `App.DevRequests.Update` | | `get_dev_request` | — | — | `App.DevRequests` | | `save_dev_request_analysis` | — | `.Write` | `App.DevRequests.Update` | @@ -1713,8 +1719,16 @@ Hangfire panosu geliştirmede serbest, üretimde `AbpHangfireAuthorizationFilter - **DevExpress Reporting**: Web Document Viewer (`/admin/reports/:report/view/...`) ve Report Designer (`/admin/reports/:report/design/...`). -- **Rapor deposu**: `ReportTemplate` ve `ReportCategory` varlıkları; `CustomReportStorageWebExtension` - ile veritabanı üzerinden okuma/yazma. +- **Rapor deposu**: `ReportTemplate` ve `ReportCategory` varlıkları + (`data/App.Reports.{ReportTemplates,Categories}.json`, seed senkronu açık). Tasarımcıda + kaydedilen `.repx` düzenleri `CustomReportStorageWebExtension` ile sunucudaki + `PredefinedReports/Reports` klasörüne yazılır. +- **`DynamicReport` şablonları** (`Raporlar` kategorisi): `HtmlContent` HTML'i A4'e basılır + (`/admin/reports/DynamicReport/view/{ŞablonId}`). Ekran satırından açılınca + (`.../view/{ŞablonId}/{ListFormCode}?recordId=@Id`) `{{AlanAdi}}` yer tutucuları kaydın form + raporundaki metniyle doldurulur (`DynamicFormReport.GetRecordTextsAsync`; ekran okuma yetkisi + + kiracı filtresi, değerler HTML kodlanır). Kiracıda bulunmayan şablon host'tan okunur. MCP + araçları §13.9. - **Dinamik raporlar** (`HttpApi.Host/PredefinedReports`): ListForm tanımından otomatik üretilen grid, form ve ağaç raporları — grid durumunu (sütun sırası, gruplama, filtre) rapora yansıtır. - Linux konteynerlerde font sorunlarını önlemek için `DevExpressFontConfiguration` devrededir. diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpReportDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpReportDtos.cs new file mode 100644 index 00000000..b1d8d313 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpReportDtos.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; + +namespace Sozsoft.Mcp.Dtos; + +/// Rapor kategorisi (App.Reports.Categories). +public class McpReportCategoryDto +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string? Icon { get; set; } +} + +/// Rapor sablonunun listede donen ozeti; HTML icerigi tasimaz. +public class McpReportTemplateSummaryDto +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string Category { get; set; } = string.Empty; + + public string? Status { get; set; } +} + +/// Kategoriler ve sablonlar birlikte; sablon kaydederken kategori adi buradan secilir. +public class McpReportCatalogDto +{ + public List Categories { get; set; } = []; + + public List Templates { get; set; } = []; +} + +/// +/// Rapor sablonunun tamami. HTML icindeki {{AlanAdi}} yer +/// tutuculardir; sablon bir ekran kaydiyla acildiginda kaydin alanlariyla doldurulur. +/// +public class McpReportTemplateDto : McpReportTemplateSummaryDto +{ + public Guid CategoryId { get; set; } + + public string HtmlContent { get; set; } = string.Empty; + + public List Placeholders { get; set; } = []; + + /// Sablonun kayitsiz goruntuleme adresi. + public string ViewUrl { get; set; } = string.Empty; + + /// + /// Kaydetme istegi bir ekran verdiyse o ekranin satir butonu (CommandColumnJson + /// elemani); ekranin mevcut butonlarina eklenir. + /// + public McpReportRowButtonDto? RowButton { get; set; } +} + +/// Sablonu bir ekran kaydiyla acan satir butonu; CommandColumnDto ile ayni alanlar. +public class McpReportRowButtonDto +{ + public string Text { get; set; } = string.Empty; + + public string Hint { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string UrlTarget { get; set; } = "_blank"; + + public string AuthName { get; set; } = string.Empty; + + public bool IsVisible { get; set; } = true; +} + +public class McpReportCategorySaveRequestDto +{ + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string? Icon { get; set; } +} + +public class McpReportTemplateSaveRequestDto +{ + /// Sablonun adi; kiraci icinde tekildir ve kimligidir. + public string Name { get; set; } = string.Empty; + + /// Kategori adi ya da Id'si. Yeni sablonda zorunlu. + public string? Category { get; set; } + + /// Basilacak HTML. Yeni sablonda zorunlu; guncellemede bos ise mevcut icerik korunur. + public string? HtmlContent { get; set; } + + public string? Description { get; set; } + + /// active ya da passive; yeni sablonda varsayilan active. + public string? Status { get; set; } + + /// + /// Sablonun basilacagi ekran. Verilirse yer tutucular ekranin alanlarina gore dogrulanir ve + /// cevapta o ekranin satir butonu doner. + /// + public string? ListFormCode { get; set; } + + /// Satir butonunun dil anahtari; verilmezse App.Platform.View. + public string? ButtonText { get; set; } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpReportAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpReportAppService.cs new file mode 100644 index 00000000..735b03ba --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpReportAppService.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// Rapor kategorisi ve rapor sablonu uretimi. Yazma App.Reports.Categories ve +/// App.Reports.ReportTemplates ekranlarinin C/U/D sozlesmesinden gecer; ekran yetkisi, +/// kiraci filtresi ve seed senkronizasyonu oradaki yolla ayni sekilde uygulanir. +/// +public interface IMcpReportAppService : IApplicationService +{ + Task GetReportTemplatesAsync(string? search); + + /// Sablonu Id'si ya da adiyla, HTML icerigi ve yer tutuculariyla doner. + Task GetReportTemplateAsync(string template); + + /// Kategoriyi ada gore kaydeder: yoksa ekler, varsa gunceller. + Task SaveReportCategoryAsync(McpReportCategorySaveRequestDto input); + + /// Sablonu ada gore kaydeder: yoksa ekler, varsa gunceller. + Task SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input); + + Task DeleteReportTemplateAsync(string template); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpReportAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpReportAppService.cs new file mode 100644 index 00000000..f36f58e6 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpReportAppService.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms.Select; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; + +namespace Sozsoft.Mcp; + +/// +/// Rapor kategorisi ve DynamicReport sablonu uretimi. Okuma depodan, yazma ekranlarin +/// C/U/D sozlesmesinden () gecer; boylece ekran yetkisi, +/// kiraci filtresi ve seed senkronizasyonu ekrandaki kayitla ayni kalir. +/// +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] +public partial class McpReportAppService( + IMcpServerConfigProvider serverConfigProvider, + IListFormDataAppService listFormDataAppService, + IMcpListFormAccessFilter accessFilter, + IRepository categoryRepository, + IRepository templateRepository, + IRepository listFormRepository, + IRepository listFormFieldRepository, + IMcpDevRequestTimesheetRecorder timesheetRecorder) + : McpAppService(serverConfigProvider), IMcpReportAppService +{ + private const string CategoriesListFormCode = PlatformConsts.AppCodes.Reports.Categories; + private const string TemplatesListFormCode = PlatformConsts.AppCodes.Reports.ReportTemplates; + private const string ActiveStatus = "active"; + private const string DefaultButtonText = "App.Platform.View"; + + [GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")] + private static partial Regex PlaceholderRegex(); + + public async Task GetReportTemplatesAsync(string? search) + { + await EnsureToolAllowedAsync(McpToolNames.ListReportTemplates); + await EnsureCanReadAsync(); + + var categories = await categoryRepository.GetListAsync(); + var categoryNames = categories.ToDictionary(x => x.Id, x => x.Name); + var templates = (await templateRepository.GetListAsync()) + .Where(x => string.IsNullOrWhiteSpace(search) || + x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || + (x.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false)) + .OrderBy(x => x.Name) + .Select(x => new McpReportTemplateSummaryDto + { + Id = x.Id, + Name = x.Name, + Description = x.Description, + Category = categoryNames.GetValueOrDefault(x.CategoryId, string.Empty), + Status = x.Status + }) + .ToList(); + + return new McpReportCatalogDto + { + Categories = [.. categories.OrderBy(x => x.Name).Select(ToDto)], + Templates = templates + }; + } + + public async Task GetReportTemplateAsync(string template) + { + await EnsureToolAllowedAsync(McpToolNames.GetReportTemplate); + await EnsureCanReadAsync(); + + var entity = await FindTemplateAsync(template) + ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); + + return await ToDtoAsync(entity, null, null); + } + + public async Task SaveReportCategoryAsync(McpReportCategorySaveRequestDto input) + { + Check.NotNull(input, nameof(input)); + await EnsureToolAllowedAsync(McpToolNames.SaveReportCategory); + + if (string.IsNullOrWhiteSpace(input.Name)) + { + throw new UserFriendlyException(L[McpErrorCodes.ReportNameRequired]); + } + + var name = input.Name.Trim(); + var existing = await categoryRepository.FirstOrDefaultAsync(x => x.Name == name); + var values = new Dictionary + { + ["Name"] = name, + ["Description"] = input.Description ?? existing?.Description, + ["Icon"] = input.Icon ?? existing?.Icon + }; + + var id = existing is null + ? await InsertAsync(CategoriesListFormCode, values) + : await UpdateAsync(CategoriesListFormCode, existing.Id, values); + + await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name); + + var saved = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id); + return saved is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(saved); + } + + public async Task SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input) + { + Check.NotNull(input, nameof(input)); + await EnsureToolAllowedAsync(McpToolNames.SaveReportTemplate); + + if (string.IsNullOrWhiteSpace(input.Name)) + { + throw new UserFriendlyException(L[McpErrorCodes.ReportNameRequired]); + } + + var name = input.Name.Trim(); + var existing = await templateRepository.FirstOrDefaultAsync(x => x.Name == name); + var htmlContent = string.IsNullOrWhiteSpace(input.HtmlContent) ? existing?.HtmlContent : input.HtmlContent; + if (string.IsNullOrWhiteSpace(htmlContent)) + { + throw new UserFriendlyException(L[McpErrorCodes.ReportHtmlRequired]); + } + + var categoryId = string.IsNullOrWhiteSpace(input.Category) + ? existing?.CategoryId + : (await FindCategoryAsync(input.Category))?.Id + ?? throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryNotFound, input.Category]); + if (categoryId is null) + { + throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryRequired]); + } + + var listForm = await FindListFormAsync(input.ListFormCode); + if (listForm is not null) + { + await EnsurePlaceholdersExistAsync(listForm.ListFormCode, htmlContent); + } + + var values = new Dictionary + { + ["Name"] = name, + ["Description"] = input.Description ?? existing?.Description, + ["HtmlContent"] = htmlContent, + ["CategoryId"] = categoryId, + ["Status"] = input.Status ?? existing?.Status ?? ActiveStatus + }; + + var id = existing is null + ? await InsertAsync(TemplatesListFormCode, values) + : await UpdateAsync(TemplatesListFormCode, existing.Id, values); + + await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name); + + var saved = await templateRepository.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, name]); + + return await ToDtoAsync(saved, listForm, input.ButtonText); + } + + public async Task DeleteReportTemplateAsync(string template) + { + await EnsureToolAllowedAsync(McpToolNames.DeleteReportTemplate); + + var entity = await FindTemplateAsync(template) + ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); + + await listFormDataAppService.PostDeleteAsync(await BuildRequestAsync( + TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)); + await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name); + } + + /// + /// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin + /// uygulamada goremedigi sablonu gostermez. + /// + private async Task EnsureCanReadAsync() + { + var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read); + await AuthorizationService.CheckAsync(permission ?? TemplatesListFormCode); + } + + private async Task InsertAsync(string listFormCode, Dictionary values) + { + var key = await listFormDataAppService.PostInsertAsync(await BuildRequestAsync( + listFormCode, AuthorizationTypeEnum.Create, values, null)); + + // Insert ucu her veri kaynaginda anahtari dondurmez; bu durumda tekil ad uzerinden bulunur. + if (Guid.TryParse(((object?)key)?.ToString(), out var id)) + { + return id; + } + + var name = (string)values["Name"]!; + return listFormCode == CategoriesListFormCode + ? (await categoryRepository.FirstAsync(x => x.Name == name)).Id + : (await templateRepository.FirstAsync(x => x.Name == name)).Id; + } + + private async Task UpdateAsync(string listFormCode, Guid id, Dictionary values) + { + await listFormDataAppService.PostUpdateAsync(await BuildRequestAsync( + listFormCode, AuthorizationTypeEnum.Update, values, id)); + return id; + } + + /// + /// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki + /// istemci hangi yetkinin eksik oldugunu gorsun. + /// + private async Task BuildRequestAsync( + string listFormCode, + AuthorizationTypeEnum operation, + Dictionary? values, + Guid? key) + { + var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation); + if (permission is not null) + { + await AuthorizationService.CheckAsync(permission); + } + + return new DataRequestDto + { + ListFormCode = listFormCode, + Data = JsonSerializer.Serialize(values ?? []), + Keys = key is null ? [] : [key.Value] + }; + } + + private async Task FindTemplateAsync(string template) + { + if (string.IsNullOrWhiteSpace(template)) + { + return null; + } + + var value = template.Trim(); + return Guid.TryParse(value, out var id) + ? await templateRepository.FirstOrDefaultAsync(x => x.Id == id) + : await templateRepository.FirstOrDefaultAsync(x => x.Name == value); + } + + private async Task FindCategoryAsync(string category) + { + var value = category.Trim(); + return Guid.TryParse(value, out var id) + ? await categoryRepository.FirstOrDefaultAsync(x => x.Id == id) + : await categoryRepository.FirstOrDefaultAsync(x => x.Name == value); + } + + private async Task FindListFormAsync(string? listFormCode) + { + if (string.IsNullOrWhiteSpace(listFormCode)) + { + return null; + } + + var code = listFormCode.Trim(); + return await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code) + ?? throw new UserFriendlyException(L[McpErrorCodes.ReportListFormNotFound, code]); + } + + /// + /// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin + /// diye kayitta reddedilir. + /// + private async Task EnsurePlaceholdersExistAsync(string listFormCode, string htmlContent) + { + var fieldNames = (await listFormFieldRepository.GetListAsync(x => x.ListFormCode == listFormCode)) + .Select(x => x.FieldName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var unknown = GetPlaceholders(htmlContent).Where(x => !fieldNames.Contains(x)).ToList(); + if (unknown.Count > 0) + { + throw new UserFriendlyException(L[McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown)]); + } + } + + private static List GetPlaceholders(string htmlContent) + => [.. PlaceholderRegex().Matches(htmlContent ?? string.Empty) + .Select(x => x.Groups[1].Value) + .Distinct(StringComparer.OrdinalIgnoreCase)]; + + private async Task ToDtoAsync(ReportTemplate entity, ListForm? listForm, string? buttonText) + { + var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == entity.CategoryId); + var viewUrl = $"/admin/reports/DynamicReport/view/{entity.Id}"; + + return new McpReportTemplateDto + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + Category = category?.Name ?? string.Empty, + CategoryId = entity.CategoryId, + Status = entity.Status, + HtmlContent = entity.HtmlContent, + Placeholders = GetPlaceholders(entity.HtmlContent), + ViewUrl = viewUrl, + RowButton = listForm is null ? null : new McpReportRowButtonDto + { + Text = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText, + Hint = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText, + Url = $"{viewUrl}/{listForm.ListFormCode}?recordId=@{listForm.KeyFieldName}", + AuthName = listForm.ListFormCode + } + }; + } + + private static McpReportCategoryDto ToDto(ReportCategory category) => new() + { + Id = category.Id, + Name = category.Name, + Description = category.Description, + Icon = category.Icon + }; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs index 7efc8a0d..e772039d 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -59,6 +59,13 @@ public static class McpErrorCodes public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired"; public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid"; public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent"; + public const string ReportNameRequired = Prefix + "ReportNameRequired"; + public const string ReportHtmlRequired = Prefix + "ReportHtmlRequired"; + public const string ReportCategoryRequired = Prefix + "ReportCategoryRequired"; + public const string ReportCategoryNotFound = Prefix + "ReportCategoryNotFound"; + public const string ReportTemplateNotFound = Prefix + "ReportTemplateNotFound"; + public const string ReportListFormNotFound = Prefix + "ReportListFormNotFound"; + public const string ReportPlaceholdersUnknown = Prefix + "ReportPlaceholdersUnknown"; public const string TableColumnsRequired = Prefix + "TableColumnsRequired"; public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType"; public const string InvalidColumnLength = Prefix + "InvalidColumnLength"; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs index 7400b6d3..3e98700a 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpTimesheetTexts.cs @@ -16,6 +16,9 @@ public static class McpTimesheetTexts public const string DeleteWizard = Prefix + "DeleteWizard"; public const string SaveComponent = Prefix + "SaveComponent"; public const string DeleteComponent = Prefix + "DeleteComponent"; + public const string SaveReportCategory = Prefix + "SaveReportCategory"; + public const string SaveReportTemplate = Prefix + "SaveReportTemplate"; + public const string DeleteReportTemplate = Prefix + "DeleteReportTemplate"; public const string InsertRow = Prefix + "InsertRow"; public const string UpdateRow = Prefix + "UpdateRow"; public const string DeleteRow = Prefix + "DeleteRow"; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs index ff0388d7..588ad079 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -38,9 +38,14 @@ public static class McpToolNames public const string GetComponent = "get_component"; public const string SaveComponent = "save_component"; public const string DeleteComponent = "delete_component"; + public const string ListReportTemplates = "list_report_templates"; + public const string GetReportTemplate = "get_report_template"; + public const string SaveReportCategory = "save_report_category"; + public const string SaveReportTemplate = "save_report_template"; + public const string DeleteReportTemplate = "delete_report_template"; /// - /// Ekran ve bilesen gelistirme araclari; kiraci baglaminda hicbiri acilmaz. Okuyanlar + /// Ekran, bilesen ve rapor sablonu gelistirme araclari; kiraci baglaminda hicbiri acilmaz. Okuyanlar /// McpServers.Invoke ile, degistirenler McpServers.Write ile acilir /// (). /// @@ -61,7 +66,12 @@ public static class McpToolNames ListComponents, GetComponent, SaveComponent, - DeleteComponent + DeleteComponent, + ListReportTemplates, + GetReportTemplate, + SaveReportCategory, + SaveReportTemplate, + DeleteReportTemplate ]; /// @@ -113,6 +123,9 @@ public static class McpToolNames [AddDevRequestSubTasks] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [SaveReportCategory] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [SaveReportTemplate] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [DeleteReportTemplate] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, }; /// diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs index 14e9c7b4..475f7df5 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs @@ -52,6 +52,7 @@ public class McpHttpApiModule : AbpModule .WithTools() .WithTools() .WithTools() + .WithTools() .WithPrompts(typeof(McpDevelopPrompts)) .WithRequestFilters(filters => { diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpReportTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpReportTools.cs new file mode 100644 index 00000000..68622e8f --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpReportTools.cs @@ -0,0 +1,80 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Sozsoft.Mcp.Dtos; + +namespace Sozsoft.Mcp.Tools; + +/// +/// Rapor kategorisi ve DynamicReport sablonu araclari. Sablon HTML'dir ve +/// /admin/reports/DynamicReport/view/{Id} ile basilir; bir ekran kaydiyla acildiginda +/// {{AlanAdi}} yer tutuculari o kaydin alanlariyla doldurulur. +/// +[McpServerToolType] +public sealed class McpReportTools(IMcpReportAppService reportAppService) +{ + [McpServerTool(Name = McpToolNames.ListReportTemplates, ReadOnly = true)] + [Description("Rapor kategorilerini ve rapor sablonlarini listeler; HTML dondurmez. Sablon kaydederken kategori adi buradan secilir.")] + public async Task ListReportTemplatesAsync( + [Description("Sablon adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")] + string? search = null) + => await reportAppService.GetReportTemplatesAsync(search); + + [McpServerTool(Name = McpToolNames.GetReportTemplate, ReadOnly = true)] + [Description("Bir rapor sablonunun HTML icerigini, yer tutucularini ve goruntuleme adresini dondurur. Degistirmeden once oku.")] + public async Task GetReportTemplateAsync( + [Description("Sablonun Id'si ya da adi.")] + string template) + => await reportAppService.GetReportTemplateAsync(template); + + [McpServerTool(Name = McpToolNames.SaveReportCategory, Destructive = true, Idempotent = true)] + [Description("Rapor kategorisini ada gore kaydeder: yoksa ekler, varsa gunceller. Kayit bir ekran kaydindan basilan HTML sablonu ise var olan 'Raporlar' kategorisini kullan; yeni kategori yalnizca istenince acilir.")] + public async Task SaveReportCategoryAsync( + [Description("Kategorinin adi; kimligidir.")] + string name, + [Description("Aciklama.")] + string? description = null, + [Description("Ikon adi.")] + string? icon = null) + => await reportAppService.SaveReportCategoryAsync(new McpReportCategorySaveRequestDto + { + Name = name, + Description = description, + Icon = icon + }); + + [McpServerTool(Name = McpToolNames.SaveReportTemplate, Destructive = true, Idempotent = true)] + [Description("DynamicReport sablonunu ada gore kaydeder: yoksa ekler, varsa gunceller; guncellemede verilmeyen alanlar korunur. Sablon A4'e basilan HTML'dir (Standard Taahhutname gibi). listFormCode verilirse HTML icindeki {{AlanAdi}} yer tutuculari o ekranin alanlarina gore dogrulanir ve cevapta rowButton doner: bu butonu ekranin CommandColumnJson listesine ekle (deploy_wizard), VisibleExpression ile kosullandir. Sozlesme icin get_dev_guide 9.12.")] + public async Task SaveReportTemplateAsync( + [Description("Sablonun adi; kimligidir.")] + string name, + [Description("Kategori adi ya da Id'si. Yeni sablonda zorunlu; kayit HTML'i icin 'Raporlar'.")] + string? category = null, + [Description("Basilacak HTML. Kayit alanlari {{AlanAdi}} ile, rapor tarihi @@date ile yazilir. Yeni sablonda zorunlu, guncellemede bos birakilirsa mevcut icerik korunur.")] + string? htmlContent = null, + [Description("Aciklama.")] + string? description = null, + [Description("'active' ya da 'passive'. Yeni sablonda varsayilan 'active'.")] + string? status = null, + [Description("Sablonun basilacagi ekranin kodu (ornegin katilimci listesi). Verilirse yer tutucular dogrulanir ve satir butonu doner.")] + string? listFormCode = null, + [Description("Satir butonunun dil anahtari. Verilmezse App.Platform.View.")] + string? buttonText = null) + => await reportAppService.SaveReportTemplateAsync(new McpReportTemplateSaveRequestDto + { + Name = name, + Category = category, + HtmlContent = htmlContent, + Description = description, + Status = status, + ListFormCode = listFormCode, + ButtonText = buttonText + }); + + [McpServerTool(Name = McpToolNames.DeleteReportTemplate, Destructive = true, Idempotent = false)] + [Description("Rapor sablonunu siler. Sablonu acan satir butonlari ekranlardan ayrica kaldirilir.")] + public async Task DeleteReportTemplateAsync( + [Description("Sablonun Id'si ya da adi.")] + string template) + => await reportAppService.DeleteReportTemplateAsync(template); +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index 094f4ad2..b22fb2ed 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -160,6 +160,9 @@ public class ListFormWizardDto public List SubForms { get; set; } = new(); public List Widgets { get; set; } = new(); + /// Toolbar ve satir butonlari (CommandColumnJson); rapor acan satir butonu da buradadir. + public List CommandColumns { get; set; } = new(); + /// Wizard'in menu/permission kodu; ad ve kod bos gelebildigi icin tek kuraldan cozulur. public string ResolveMenuCode() { @@ -219,6 +222,7 @@ public class ListFormWizardDto var groups = Groups ?? []; var subForms = SubForms ?? []; var widgets = Widgets ?? []; + var commandColumns = CommandColumns ?? []; var editingFormDtos = groups .Select((g, gi) => new { Group = g, Order = gi + 1 }) @@ -302,6 +306,7 @@ public class ListFormWizardDto dto.EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null; dto.SubFormsJson = subForms.Count > 0 ? JsonSerializer.Serialize(subForms) : null; dto.WidgetsJson = widgets.Count > 0 ? JsonSerializer.Serialize(widgets) : null; + dto.CommandColumnJson = commandColumns.Count > 0 ? JsonSerializer.Serialize(commandColumns) : null; dto.WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null; dto.TreeOptionJson = UsesLayout("tree", Tree) && !string.IsNullOrEmpty(TreeOptionDto?.ParentIdExpr) @@ -428,6 +433,7 @@ public class ListFormWizardDto WorkflowDto = listForm.WorkflowDto; SubForms = [.. listForm.SubFormsDto]; Widgets = [.. listForm.WidgetsDto]; + CommandColumns = [.. listForm.CommandColumnDto]; Groups = BuildGroups(listForm, fields); } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index 852cae6d..5201adad 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -93,6 +93,7 @@ public class WizardSeedFileDto nameof(ListFormWizardDto.TreeOptionDto), nameof(ListFormWizardDto.GanttOptionDto), nameof(ListFormWizardDto.SchedulerOptionDto), nameof(ListFormWizardDto.TodoOptionDto), nameof(ListFormWizardDto.WorkflowDto), nameof(ListFormWizardDto.SubForms), nameof(ListFormWizardDto.Widgets), + nameof(ListFormWizardDto.CommandColumns), ]; /// Grup ve alan tanimlarinin bolumunde karsiligi olan anahtarlari. diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs index 1c652530..136e1f04 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Administration.cs @@ -4120,9 +4120,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportCategory)), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), - SeedSyncInsert = false, - SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, KeyFieldName = "Id", DefaultFilter = DefaultFilterJson, KeyFieldDbSourceType = DbType.Guid, @@ -4258,9 +4258,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportTemplate)), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), - SeedSyncInsert = false, - SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 477ea9f1..36f8d792 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -70,18 +70,72 @@ public class DynamicFormReport : XtraReport } using var scope = _scopeFactory.CreateScope(); - var authManager = scope.ServiceProvider.GetRequiredService(); - var listFormManager = scope.ServiceProvider.GetRequiredService(); - var listFormFieldManager = scope.ServiceProvider.GetRequiredService(); - var customizationManager = scope.ServiceProvider.GetRequiredService(); - var dynamicDataManager = scope.ServiceProvider.GetRequiredService(); - var selectQueryManager = scope.ServiceProvider.GetRequiredService(); - var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); var configuration = scope.ServiceProvider.GetRequiredService(); var currentUser = scope.ServiceProvider.GetRequiredService(); var isSubReport = DynamicReportImageHelper.IsSubReport(parameters); + var (listForm, reportFields, selectQuery, row, connectionString, dataSourceType) = await QueryRecordAsync( + scope.ServiceProvider, + listFormCode, + id, + DynamicReportImageHelper.GetParameter(parameters, "filter")); + + var formGroups = GetFormGroups(listForm, reportFields).ToList(); + var listSubForms = GetListSubForms(listForm, row).ToList(); + var subReports = await CreateSubReportsAsync(listSubForms); + var requiresLandscape = HasWideFormLayout(formGroups) || + subReports.Any(x => x.Report.RequiresLandscapeLayout); + + ConfigurePage(requiresLandscape); + ConfigureDataSource(selectQuery, connectionString, dataSourceType); + BuildLayout(listForm, formGroups, row, subReports, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName, isSubReport); + RequestParameters = false; + } + + /// + /// Kaydin ekranda okunabilen alanlarini, form raporunun bastigi metinle doner (lookup gorunen + /// metni, statik lookup karsiligi, tarih/sayi bicimi). Anahtar alan adidir. Ekran okuma yetkisi + /// ve kiraci filtresi form raporundaki yoldan aynen uygulanir; kayit yoksa null doner. + /// + public static async Task> GetRecordTextsAsync(string listFormCode, string id) + { + if (_scopeFactory == null) + { + throw new InvalidOperationException("DynamicFormReport service provider is not configured."); + } + + using var scope = _scopeFactory.CreateScope(); + var (_, reportFields, _, row, _, _) = await QueryRecordAsync(scope.ServiceProvider, listFormCode, id, null); + if (row == null) + { + return null; + } + + var texts = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var field in reportFields) + { + texts[field.Field.FieldName] = TryGetFormattedValue(field, row, out var text) ? text : string.Empty; + } + + return texts; + } + + private static async Task<(ListForm ListForm, List ReportFields, string SelectQuery, IDictionary Row, string ConnectionString, DataSourceTypeEnum DataSourceType)> QueryRecordAsync( + IServiceProvider serviceProvider, + string listFormCode, + string id, + string filter) + { + var authManager = serviceProvider.GetRequiredService(); + var listFormManager = serviceProvider.GetRequiredService(); + var listFormFieldManager = serviceProvider.GetRequiredService(); + var customizationManager = serviceProvider.GetRequiredService(); + var dynamicDataManager = serviceProvider.GetRequiredService(); + var selectQueryManager = serviceProvider.GetRequiredService(); + var defaultValueHelper = serviceProvider.GetRequiredService(); + var localizer = serviceProvider.GetRequiredService>(); + if (!await authManager.CanAccess(listFormCode)) { throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'."); @@ -100,7 +154,7 @@ public class DynamicFormReport : XtraReport var queryParams = new QueryParameters { ListFormCode = listFormCode, - Filter = DynamicReportImageHelper.GetParameter(parameters, "filter") ?? CreateKeyFilter(listForm.KeyFieldName, id), + Filter = filter ?? CreateKeyFilter(listForm.KeyFieldName, id), Take = 1, Skip = 0 }; @@ -119,17 +173,7 @@ public class DynamicFormReport : XtraReport dataSourceType); var row = GetFirstRow(await dynamicDataRepository.QueryAsync(selectQuery, connectionString)); - - var formGroups = GetFormGroups(listForm, reportFields).ToList(); - var listSubForms = GetListSubForms(listForm, row).ToList(); - var subReports = await CreateSubReportsAsync(listSubForms); - var requiresLandscape = HasWideFormLayout(formGroups) || - subReports.Any(x => x.Report.RequiresLandscapeLayout); - - ConfigurePage(requiresLandscape); - ConfigureDataSource(selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, formGroups, row, subReports, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName, isSubReport); - RequestParameters = false; + return (listForm, reportFields, selectQuery, row, connectionString, dataSourceType); } private void ConfigurePage(bool landscape) diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index 9658de89..ab22df8e 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -10,7 +10,9 @@ using DevExpress.DataAccess.Sql; using DevExpress.XtraPrinting; using Microsoft.Extensions.DependencyInjection; using Sozsoft.Platform.Entities; +using Volo.Abp.Data; using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; using Volo.Abp.Users; namespace Sozsoft.Platform.ReportServices; @@ -21,6 +23,9 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens { readonly string reportDirectory = "PredefinedReports\\Reports"; const string FileExtension = ".repx"; + const string RecordIdParameter = "recordId"; + private static readonly System.Text.RegularExpressions.Regex RecordPlaceholderRegex = + new(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}", System.Text.RegularExpressions.RegexOptions.Compiled); private readonly IServiceScopeFactory _scopeFactory; public CustomReportStorageWebExtension(IServiceScopeFactory scopeFactory) @@ -243,13 +248,14 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens } using var scope = _scopeFactory.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService>(); - var template = await repository.FindAsync(reportTemplateId); + var template = await FindReportTemplateAsync(scope.ServiceProvider, reportTemplateId); if (template == null) { throw new InvalidOperationException($"Report template '{reportTemplateId}' was not found."); } + var htmlContent = await MergeRecordAsync(template.HtmlContent, parameters); + // A stored DynamicReport layout can contain a named SqlDataSource. The cached document // builder executes that source later on a background thread, where its connection can be // stale or no longer carry the request's tenant context. Embed the tenant-scoped result in @@ -270,7 +276,7 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens Name = "DynamicReportData", JsonSource = new CustomJsonSource(JsonSerializer.Serialize(new[] { - new { template.HtmlContent } + new { HtmlContent = htmlContent } })) }; dataSource.Fill(); @@ -280,6 +286,50 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens report.DataMember = null; } + /// + /// Kiracinin kendi sablonu once aranir; yoksa host sablonu kullanilir. Host sablonlari urunle + /// gelen ortak tasarimlardir (seed) ve yalnizca HTML tasir; basilan veri her zaman kiracinin + /// kendi ekranindan okunur. + /// + private static async Task FindReportTemplateAsync(IServiceProvider serviceProvider, Guid reportTemplateId) + { + var repository = serviceProvider.GetRequiredService>(); + var template = await repository.FindAsync(reportTemplateId); + if (template != null) + { + return template; + } + + var dataFilter = serviceProvider.GetRequiredService(); + using (dataFilter.Disable()) + { + return await repository.FindAsync(x => x.Id == reportTemplateId && x.TenantId == null); + } + } + + /// + /// Sablon bir ekran kaydiyla acildiysa (DynamicReport/view/{sablon}/{listFormCode}?recordId=) + /// {{AlanAdi}} yer tutuculari kaydin form raporundaki metniyle doldurulur. Degerler HTML + /// olarak kodlanir; kayit verisi sablonun isaretlemesine karisamaz. + /// + private static async Task MergeRecordAsync(string htmlContent, IReadOnlyDictionary parameters) + { + if (string.IsNullOrEmpty(htmlContent) || + !parameters.TryGetValue("listFormCode", out var listFormCode) || string.IsNullOrWhiteSpace(listFormCode) || + !parameters.TryGetValue(RecordIdParameter, out var recordId) || string.IsNullOrWhiteSpace(recordId)) + { + return htmlContent; + } + + var texts = await Sozsoft.Reports.PredefinedReports.DynamicFormReport.GetRecordTextsAsync(listFormCode, recordId) + ?? throw new InvalidOperationException($"Record '{recordId}' was not found in '{listFormCode}'."); + + return RecordPlaceholderRegex.Replace(htmlContent, match => + texts.TryGetValue(match.Groups[1].Value, out var text) + ? System.Net.WebUtility.HtmlEncode(text) + : match.Value); + } + private void ApplyDynamicReportFooter(XtraReport report) { var bottomMargin = report.Bands.OfType().FirstOrDefault(); @@ -425,6 +475,17 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens return; } + if (IsDynamicReport(reportName)) + { + parameters.TryAdd("id", pathParameters[0]); + if (pathParameters.Length > 1) + { + parameters.TryAdd("listFormCode", pathParameters[1]); + } + + return; + } + if (IsDynamicGridReport(reportName) || IsDynamicTreeReport(reportName) || IsDynamicFormReport(reportName)) { if (pathParameters.Length == 1) diff --git a/claude.md b/claude.md index c4223ef0..06d5d029 100644 --- a/claude.md +++ b/claude.md @@ -193,7 +193,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → taşır. `ListForm`/`Fields` içinde karşılığı olan her alan (veri kaynağı, select, anahtar alan, kırılım/düzen/düzenleme bayrakları, görünüm option blokları, alt form, widget, iş akışı, alan tanımları) dosyaya yazılmadan çıkarılır (`WizardSeedFileDto.ToSeedJson`) ve okurken geri - doldurulur (`FromSeedJson`). Wizard'a yeni bir alan eklerken karşılığı ListForm'da varsa listeye ekle. + doldurulur (`FromSeedJson`). Wizard'a yeni bir alan eklerken karşılığı ListForm'da varsa listeye ekle (`CommandColumns` → + `CommandColumnJson` bu yolla taşınır; wizard ekranında adımı yoktur, UI modeli düzenlemede korur). Dosyada kalan wizard alanları üç başlık altında toplanır — `Menu`, `Language`, `Groups` — ve `ListFormCode` → `WizardCode`, `MenuParentModuleId` → `ModuleId`, `LanguageTexts` → `Language.Texts` adıyla yazılır; varsayılanı olan alanlar (`CreateMenu: true`, boş `DataSourceConnectionString`, boş @@ -279,7 +280,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → sunucu doldurur, modele bırakılmaz. `Timesheets`'i tamamen sunucu yazar (`IMcpDevRequestTimesheetRecorder`): `get_dev_request`/`start_dev_request` talebi etkin talep yapar, sonraki her üretim/veri yazma işlemi (`create_table`, `deploy_wizard`, `delete_wizard`, - `save_component`, `delete_component`, satır araçları, `save_dev_request_analysis`, + `save_component`, `delete_component`, `save_report_category`, `save_report_template`, + `delete_report_template`, satır araçları, `save_dev_request_analysis`, `append_dev_request_note`, `add_dev_request_subtasks`) ona `App.Mcp.Timesheet.*` anahtarından Türkçe açıklama ve ölçülen süreyle bir satır ekler; yalnızca talebi açan `start_dev_request` satır yazmaz. Yeni bir üretim/veri yazma aracı eklenirse kaydedici çağrısı ve dil anahtarı da eklenir. Satır aracın UoW'si commit edildikten sonra ayrı UoW'de yazılır; aynı UoW'de yazmak Dapper diff --git a/configs/seeds/host/data/App.DevRequests/c8017276-eef2-4ede-b060-ec86339fee18.json b/configs/seeds/host/data/App.DevRequests/c8017276-eef2-4ede-b060-ec86339fee18.json new file mode 100644 index 00000000..ceebc420 --- /dev/null +++ b/configs/seeds/host/data/App.DevRequests/c8017276-eef2-4ede-b060-ec86339fee18.json @@ -0,0 +1,26 @@ +{ + "ListFormCode": "App.DevRequests", + "KeyFieldName": "Id", + "GeneratedAt": "2026-10-01T10:29:00.7111349Z", + "Order": 0, + "Rows": [ + { + "Id": "c8017276-eef2-4ede-b060-ec86339fee18", + "TenantId": null, + "UserName": "system@sozsoft.com", + "RequestDate": null, + "Status": "Backlog", + "Title": "Human Resources -> Eğitim", + "Description": "

Human Resources menüsünün içerisinde Eğitim isimli bir alt menü oluşturulmalı. Aşağıdaki komponentler bu alt menünün altına yerleştirilmeli.


Eğitim Konuları : Eğtiim Tipi sütunu Oryantasyon Eğitimi, İş Başı Eğitimi, Teknik Oryantasyon Eğitimi, Grup Eğitimi, Firma Dışı Eğitimi, Eğitim İçerikli Sosyal Faaliyet, Akademi Eğitimleri, Rotasyon Eğitimleri seçenekleri olmalı.



Eğitimler : Yukarıda bahsedilen eğitim tiplerine ait fotoğraflarını gönderiyorum. Ancak beklentim sadece bir eğitim ekranına ihtiyacım var. Bu resimleri birleştirerek tek bir komponent tasarlamalısın. Eğitim ekranının üzerinde Eğitim tipi seçilmeli ve yukarıdaki listedeki Eğitim Konusu seçilmeli. Diğer sütunlar resimlerdeki ortak ve artan tüm bilgileri içerecek şekilde oluşturulmalıdır. Eğitimler listesinin detayında Katılımcılar, Maliyet ve Yetkinlikler girilebilmeli. Yekinlikler -> Skill Types içerisinde seçilebilmeli hatta bunun listesi olmayabilir Eğitim kaydında birden fazla seçilebilen yetkinler sütunu da olabilir. Her katılımcının katılıma durumu, değerlendirme puanı, sınav notu gibi bilgiler girilebilmelidir. Eğitimle alakalı birden fazla eğitim kaynağı olan dijital dosyaları da bu eğtiimin içerisine kaydedebilmeliyim.


Eğitime katılan tüm katılımcılar eğer katıldıysa Sertifika alabilmeli. Sertifika raporu aşağıdaki gibi hazırlanmalı. Uygulamada Rapor Kategorileri, Report Templates seçenekleri var. Bu Sertifika raporu için Standart

Taahhütname gibi bir rapor hazırlanabilir. DynamicReport ile hazırlanabilmeli. Raporlar kategorisinde Standard Taahhütname gibi statik bir rapor ekle



Yine eğitimin içerisinde Maliyet bilgileri girebileceğimiz bir group olmalı. aşağıda resimdeki detayları o kısma yazabilmeliyim. Bu bilgiler Eğitim listesinde görünebilmelidir.



", + "DueDate": "2026-10-01T10:34:00", + "Tags": "HR", + "Assignees": "system@sozsoft.com", + "Priority": "5", + "Completed": false, + "SortOrder": 0, + "Timesheets": null, + "SubTasks": null, + "Analysis": "" + } + ] +} diff --git a/configs/seeds/host/data/App.Languages.LanguageText.json b/configs/seeds/host/data/App.Languages.LanguageText.json index 0bee6b25..9cfdbbbe 100644 --- a/configs/seeds/host/data/App.Languages.LanguageText.json +++ b/configs/seeds/host/data/App.Languages.LanguageText.json @@ -67951,14 +67951,14 @@ "CultureName": "en", "ResourceName": "Platform", "Key": "App.Reports.ReportTemplates.Description", - "Value": "Report Templates — printouts with their own layout. Table: Adm_T_ReportTemplate · tenant scope · list screen. Reporting is based on DevExpress XtraReports.\nUsage: needed for output with its own layout (invoice, quotation, work order, certificate). The template is drawn in the designer at /admin/reports/{report}/design and viewed at /admin/reports/{report}/view/{id}. It is bound to a screen with a CommandColumnJson button: Url = \"/admin/reports/{ReportName}/view/@Id\".\nFields:\n• Name → the template name; the report is opened with this value\n• CategoryId → the category it belongs to; the lookup comes from the Report Categories screen\n• Description → what the report is for and which data set it uses\n• HtmlContent → the layout definition of the template; produced by the designer, not edited by hand\n• Status → the state of the template; a draft template is not offered to the user\nNote: for a plain printout of the current list no definition is needed — the report action in the toolbar of every list opens the ready DynamicGrid report (DynamicTree in a tree list) with the current filters and columns.\nCaution: report templates are database records; they are not written into configs/seeds/. Take an export before a large change — the designer output cannot be diffed easily." + "Value": "Report Templates — printouts with their own layout. Table: Adm_T_ReportTemplate · tenant scope · list screen. Reporting is based on DevExpress XtraReports.\nUsage: needed for output with its own layout (invoice, quotation, work order, certificate). The template is drawn in the designer at /admin/reports/{report}/design and viewed at /admin/reports/{report}/view/{id}. It is bound to a screen with a CommandColumnJson button: Url = \"/admin/reports/{ReportName}/view/@Id\".\nFields:\n• Name → the template name; the report is opened with this value\n• CategoryId → the category it belongs to; the lookup comes from the Report Categories screen\n• Description → what the report is for and which data set it uses\n• HtmlContent → the layout definition of the template; produced by the designer, not edited by hand\n• Status → the state of the template; a draft template is not offered to the user\nNote: for a plain printout of the current list no definition is needed — the report action in the toolbar of every list opens the ready DynamicGrid report (DynamicTree in a tree list) with the current filters and columns.\nCaution: template records are mirrored to configs/seeds/ (data/App.Reports.ReportTemplates.json). A template in the Raporlar category is HTML printed by DynamicReport; when it is opened from a screen row (/admin/reports/DynamicReport/view/{Id}/{ListFormCode}?recordId=@Id) its {{FieldName}} placeholders are filled from that record. Over MCP: list_report_templates, get_report_template, save_report_category, save_report_template." }, { "Id": "eeebfb5c-35b9-5c8e-b23e-556d35948928", "CultureName": "tr", "ResourceName": "Platform", "Key": "App.Reports.ReportTemplates.Description", - "Value": "Rapor Şablonları — kendi düzeni olan çıktılar. Tablo: Adm_T_ReportTemplate · tenant kapsamlı · liste ekranı. Raporlama DevExpress XtraReports tabanlıdır.\nKullanım: kendi düzeni istenen çıktı (fatura, teklif, iş emri, sertifika) için gerekir. Şablon /admin/reports/{rapor}/design adresindeki tasarımcıda çizilir, /admin/reports/{rapor}/view/{id} adresinde görüntülenir. Ekrana CommandColumnJson butonu ile bağlanır: Url = \"/admin/reports/{RaporAdi}/view/@Id\".\nAlanlar:\n• Name → şablon adı; rapor bu değerle açılır\n• CategoryId → bağlı olduğu kategori; lookup Rapor Kategorileri ekranından gelir\n• Description → raporun ne işe yaradığı ve hangi veri setini kullandığı\n• HtmlContent → şablonun düzen tanımı; tasarımcı tarafından üretilir, elle düzenlenmez\n• Status → şablonun durumu; taslak şablon kullanıcıya sunulmaz\nNot: o anki listenin düz çıktısı için tanım gerekmez — her listenin araç çubuğundaki rapor aksiyonu hazır DynamicGrid raporunu (ağaç listede DynamicTree) mevcut filtreler ve kolonlarla açar.\nDikkat: rapor şablonları veritabanı kaydıdır; configs/seeds/ altına yazılmaz. Büyük bir değişiklikten önce dışa aktarım al — tasarımcı çıktısı kolay karşılaştırılamaz." + "Value": "Rapor Şablonları — kendi düzeni olan çıktılar. Tablo: Adm_T_ReportTemplate · tenant kapsamlı · liste ekranı. Raporlama DevExpress XtraReports tabanlıdır.\nKullanım: kendi düzeni istenen çıktı (fatura, teklif, iş emri, sertifika) için gerekir. Şablon /admin/reports/{rapor}/design adresindeki tasarımcıda çizilir, /admin/reports/{rapor}/view/{id} adresinde görüntülenir. Ekrana CommandColumnJson butonu ile bağlanır: Url = \"/admin/reports/{RaporAdi}/view/@Id\".\nAlanlar:\n• Name → şablon adı; rapor bu değerle açılır\n• CategoryId → bağlı olduğu kategori; lookup Rapor Kategorileri ekranından gelir\n• Description → raporun ne işe yaradığı ve hangi veri setini kullandığı\n• HtmlContent → şablonun düzen tanımı; tasarımcı tarafından üretilir, elle düzenlenmez\n• Status → şablonun durumu; taslak şablon kullanıcıya sunulmaz\nNot: o anki listenin düz çıktısı için tanım gerekmez — her listenin araç çubuğundaki rapor aksiyonu hazır DynamicGrid raporunu (ağaç listede DynamicTree) mevcut filtreler ve kolonlarla açar.\nDikkat: şablon kayıtları configs/seeds/ altına (data/App.Reports.ReportTemplates.json) yansır. Raporlar kategorisindeki şablon DynamicReport ile basılan HTML'dir; bir ekran satırından açıldığında (/admin/reports/DynamicReport/view/{Id}/{ListFormCode}?recordId=@Id) {{AlanAdi}} yer tutucuları o kaydın alanlarıyla doldurulur. MCP üzerinden: list_report_templates, get_report_template, save_report_category, save_report_template." }, { "Id": "613a3f66-1426-5a8f-af24-4354b9dc6318", @@ -69318,6 +69318,48 @@ "Key": "App.Mcp.Timesheet.DeleteComponent", "Value": "{0} bileşeni silindi" }, + { + "Id": "e7c8404a-ba29-52f2-9f48-53d778dfe2f9", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveReportCategory", + "Value": "Report category {0} saved" + }, + { + "Id": "744aed7c-4aa9-5868-a967-362543d4b9da", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveReportCategory", + "Value": "{0} rapor kategorisi kaydedildi" + }, + { + "Id": "d17347c8-f526-560c-bd68-a9323721ffd9", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveReportTemplate", + "Value": "Report template {0} saved" + }, + { + "Id": "3adca18e-583c-51d5-8e4d-4206f58ff3a3", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.SaveReportTemplate", + "Value": "{0} rapor şablonu kaydedildi" + }, + { + "Id": "c03489b9-1434-5598-bfec-f928e163b0d7", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteReportTemplate", + "Value": "Report template {0} deleted" + }, + { + "Id": "93e25831-412f-528b-8fec-6553b5d1137f", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Timesheet.DeleteReportTemplate", + "Value": "{0} rapor şablonu silindi" + }, { "Id": "11a42b2b-3a91-54ec-92c7-3020e0b3aa14", "CultureName": "en", @@ -69416,6 +69458,104 @@ "Key": "App.Mcp.Error.VisualDesignerComponent", "Value": "\"{0}\" bileşeninin kodu Visual Designer belgesinden üretiliyor, MCP üzerinden yazılamaz. Bileşeni Visual Designer'da düzenleyin." }, + { + "Id": "e9467c21-1acf-5b7e-bafe-22f5327e80b4", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportNameRequired", + "Value": "A name is required." + }, + { + "Id": "d61f69b0-af46-5a66-bb64-c1558fecc904", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportNameRequired", + "Value": "Ad zorunludur." + }, + { + "Id": "23b03a6a-9131-5367-a32c-3c4e6506f5aa", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportHtmlRequired", + "Value": "HtmlContent is required for a new report template." + }, + { + "Id": "e406540b-d85e-5cf7-9076-6250f1c0532f", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportHtmlRequired", + "Value": "Yeni rapor şablonunda HtmlContent zorunludur." + }, + { + "Id": "18d9c5cc-176e-5de9-a5a9-dd03bcc65934", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportCategoryRequired", + "Value": "A category is required for a new report template. Read the categories with list_report_templates." + }, + { + "Id": "c31266e3-00be-5f7a-9c23-2cd7842c68fa", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportCategoryRequired", + "Value": "Yeni rapor şablonunda kategori zorunludur. Kategorileri list_report_templates ile okuyun." + }, + { + "Id": "28fe03db-7c92-5875-a41f-c5c03299636e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportCategoryNotFound", + "Value": "Report category \"{0}\" was not found. Read the categories with list_report_templates or create it with save_report_category." + }, + { + "Id": "fa2a2230-8000-5326-ba1e-7a014cc45f39", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportCategoryNotFound", + "Value": "\"{0}\" rapor kategorisi bulunamadı. Kategorileri list_report_templates ile okuyun ya da save_report_category ile oluşturun." + }, + { + "Id": "aaf964c0-ea3b-54a7-931c-95978e34e05e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportTemplateNotFound", + "Value": "Report template \"{0}\" was not found." + }, + { + "Id": "664b422e-00f3-5b86-a4f0-6a6b6205ccc8", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportTemplateNotFound", + "Value": "\"{0}\" rapor şablonu bulunamadı." + }, + { + "Id": "375f2d7b-53c6-51f9-9682-aa5b17ffd334", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportListFormNotFound", + "Value": "List form \"{0}\" was not found. A screen that is waiting for DB Migrate cannot be used yet." + }, + { + "Id": "134eb4f8-8902-5da5-a44d-b35a8bab0b5a", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportListFormNotFound", + "Value": "\"{0}\" ekranı bulunamadı. DB Migrate bekleyen bir ekran henüz kullanılamaz." + }, + { + "Id": "fff89062-9058-5067-a13e-d2cb6f831dca", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportPlaceholdersUnknown", + "Value": "These placeholders are not fields of \"{0}\": {1}. Use the field names returned by describe_list." + }, + { + "Id": "f6791258-c8e7-5351-b0e4-12e167be862f", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Mcp.Error.ReportPlaceholdersUnknown", + "Value": "Şu yer tutucular \"{0}\" ekranının alanı değil: {1}. describe_list ile dönen alan adlarını kullanın." + }, { "Id": "66fc88f1-60bd-5e69-9dbc-2da3a8db6f57", "CultureName": "en", diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index a2263f11..bbf4c48d 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -1,5 +1,6 @@ import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models' import { + CommandColumnDto, GanttOptionDto, GridOptionsEditDto, SchedulerOptionDto, @@ -128,6 +129,8 @@ export interface ListFormWizardDto { groups?: ListFormWizardColumnGroupDto[] subForms?: SubFormDto[] widgets?: WidgetEditDto[] + /** Wizard adımı yoktur; düzenlemede korunur ve MCP deploy_wizard ile yazılır. */ + commandColumns?: CommandColumnDto[] } /** Izin grubu secim listesi ogesi; gorunen adlar grup adiyla ayni olan dil anahtarindan gelir. */ diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index 447f8ab0..b8d49aba 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -181,6 +181,7 @@ const initialValues: ListFormWizardDto = { keyFieldDbSourceType: DbTypeEnum.Int32, subForms: [], widgets: [], + commandColumns: [], } const step1ValidationSchema = Yup.object().shape({ @@ -753,6 +754,7 @@ const Wizard = () => { keyFieldDbSourceType: w.keyFieldDbSourceType ?? DbTypeEnum.Int32, subForms: w.subForms ?? [], widgets: w.widgets ?? [], + commandColumns: w.commandColumns ?? [], }) // Queue column load to run once dbObjects is available @@ -1139,6 +1141,7 @@ const Wizard = () => { groups: [], subForms: [], widgets: [], + commandColumns: [], }) returnToManager()