# Low-Code Üretim Referansı (Authoring Reference) Bu doküman, bir yapay zekâ ajanının **platformu bir geliştirici gibi kullanarak** ekran ve bileşen üretebilmesi için gereken somut şemaları, alan anlamlarını, enum değerlerini ve çalışan örnekleri içerir. - Ne yapılacağına karar verme kuralları: `ai.instructions.md` - `api/` kod standardı: `dotnet.instructions.md` - Modül/liste ekleme prosedürü: `list.instructions.md` - Neyin nerede yaşadığı: `../../README.md` Çelişki olursa `ai.instructions.md` kazanır. Buradaki her şey **"kod yazmadan üretim"** tarafındadır; bu dosya platformun çekirdek kodunu geliştirmek için değil, **prompt ile uygulamaya yeni ekran, menü, yetki, dil anahtarı, route ve süreç eklemek** içindir. Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına uygulamanın kendi üreteceği dosyaların aynılarını yazmak esastır — bkz. §0. > **Altın kural:** Ürettiğin her şey bir seed dosyasına düşmelidir. Veritabanı silinip > yeniden oluşturulduğunda geri gelmeyen bir artefakt teslim edilmiş sayılmaz. --- ## 0. Üretim protokolü (her prompt için zorunlu) Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi** davran: Wizard'ın, Component Manager'ın ve SQL Query Manager'ın diske yazacağı dosyaların aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur. Talep iki biçimde gelir ve ikisi de aynı hatta girer: - **Yazılı tarif** → §0.6 ile ekran tarifine çevir. - **Ekran görüntüsü / mockup / fotoğraf** → §0.7 ile önce görseli oku, tarifi çıkar. Tarif hazır olunca §0.6.1 ile yolu seç (Wizard mı, Custom Component mi), sonra §0.2'deki sırayla dosyaları üret. ### 0.1 Yazılacak klasörler ``` configs/seeds/host/ ├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server) ├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon) ├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise) ├── crud/{EntityName}.json ← tablonun REST uçları ├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod) └── wizard/{yyyyMMddHHmmss}_{WizardName}.json ← ekran + menü + yetki + dil anahtarları ``` Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır. ### 0.2 Üretim sırası — bu sırayla yaz, bu sırayla çalışır | # | Adım | Dosya | Neden bu sırada | | --- | --- | --- | --- | | 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı | | 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | | 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar | | 4 | Wizard | `wizard/{ts}_{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir | | 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** zaman damgasını alır | **Zaman damgası bir bağımlılık aracıdır.** `WizardDataSeeder` `wizard/` klasörünü **dosya adına göre sıralı** işler. Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa, bağımlı olanın zaman damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz ekranı her zaman önce numaralandır. ### 0.3 Yazmadan önce zorunlu keşif Uydurma. Şu üçünü mutlaka kontrol et: 1. **Tablo/kolon adları** — `sql/object/` altındaki dosyalar ya da mevcut wizard dosyalarındaki `SelectCommand`. Kolon adını tahmin etme; yoksa tabloyu da sen üret. 2. **Var olan menü kökü ve izin grubu** — aynı modül için ikinci bir kök menü açma. Mevcut wizard dosyalarındaki `MenuParentCode` / `PermissionGroupName` değerlerini tara. 3. **Dil anahtarı çakışması** — `LanguagesData.json` ve diğer wizard dosyaları. Var olan bir anahtarın metnini seed **ezmez**; yanlış metinle karşılaşırsan anahtarı değiştir. ### 0.4 Üretim sonrası - Değişikliklerin devreye girmesi için **DB Migrate** (SQL Query Manager / Wizard Manager üzerindeki buton, yetki `App.Setup.Migrate`) ya da migrator konteyneri (`SEED=true`) çalışmalı. - Seeder **idempotenttir**: ekran zaten varsa dosya atlanır (§4.5). Var olan bir ekranı değiştirmek için dosyayı düzenlemek yetmez — kayıtları silip yeniden seed etmek ya da ekranı Wizard'dan `EditFileName` ile yeniden çalıştırmak gerekir. - Ürettiğin dosyaları kullanıcıya **tek tek listele**: hangi dosya, ne üretiyor, hangi menüde görünecek, hangi yetkiyi ister. ### 0.5 Dosya biçimi kuralları | Kural | Değer | | --- | --- | | Kodlama | UTF-8 (BOM'suz) | | JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir | | Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) | | Enum'lar | Sayı olarak yazılır (§3) | | `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı | | İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır | | Satır sonu | LF | --- ### 0.6 Talebi ekran tarifine çevir Dosya yazmadan önce **tek bir ekran tarifi** çıkar ve kullanıcıya kısaca doğrulat. Tarif, hangi aracın kullanılacağını (Wizard mı, Custom Component mi) ve hangi dosyaların üretileceğini belirler. **Ekran tarifi şablonu** — her maddeye bir satır, bilinmiyorsa varsayımını yaz: ``` Amaç : Ne işi görüyor, kim kullanıyor Veri kaynağı : Tablo/view adı · yoksa "üretilecek" Anahtar : Kolon + tip Sütunlar : Alan → editör tipi → zorunlu mu → lookup kaynağı Layout(lar) : Grid / Card / Pivot / Chart / Tree / Gantt / Scheduler / Todo Form düzeni : Gruplar ve kolon sayısı Aksiyonlar : Toolbar ve satır butonları, ne yapıyorlar Alt ekranlar : Ana-detay sekmeleri Widget'lar : Üstteki KPI kartları Akış : Onay/koşul adımları Menü : Üst menü, sıra, ikon Yetki : Grup adı + kimde hangi yetki Tenant : IsTenant / IsBranch / IsOrganizationUnit Dil : EN + TR metinler ``` Kural: **eksik bilgi için işi durdurma.** Varsayımını tarifin içine yaz, üret, sonunda "şu varsayımlarla ürettim" diye bildir. Yalnızca yanlış varsayımın veri kaybettireceği yerlerde (hangi tabloya yazılacağı, kimin onaylayacağı) sor. #### 0.6.1 Hangi araç? — yol seçimi Tarifi çıkardıktan sonra tek bir karar kalır: | Tarifte varsa | Yol | | --- | --- | | Kayıt listesi + ekle/düzenle/sil + filtre/arama | **SQL Query Manager + Wizard** (`ComponentKind: 0`) | | Aynı verinin birden çok görünümü (pano, takvim, ağaç, grafik) | **Wizard** — tek dosya, birden çok layout (§9.8) | | Ana kayıt + detay sekmeleri | **Wizard** + `SubForms` | | Onay süreci | **Wizard** + `WorkflowDto` | | Üstte KPI kartları | **Wizard** + `Widgets` | | Serbest yerleşim: yan yana kartlar, özel bölümler, sihirbaz adımları, karışık bileşenler | **Custom Component** (`ComponentKind: 1`) | | Var olan ekranların bir sayfada toplanması (dashboard) | **Custom Component** + `platform` düğümleri (`listFormCode`) | | Tek kayıt üzerinde çalışan özel form (liste yok) | **Custom Component** + `Form` düğümü | Şüphede kalırsan **Wizard'ı seç.** Grid'e benzeyen her şey Wizard'la daha ucuz, daha yetkin ve daha bakımlıdır; Custom Component'e yalnızca yerleşim gerçekten grid/form kalıbına sığmıyorsa geç. İki yol birlikte de kullanılır: veriyi Wizard ile ekran yap, sonra Custom Component içine `platform` düğümü olarak göm. --- ### 0.7 Görselden üretim (ekran görüntüsü / mockup) Kullanıcı tarif yerine **bir fotoğraf, ekran görüntüsü ya da mockup** gönderdiğinde önce görseli okuyup §0.6'daki tarifi çıkar, sonra dosyaları üret. Görseli tahmine değil, gördüğün öğelere dayandır. #### 0.7.1 Görseli okuma sırası 1. **Sayfa iskeleti** — kaç bölge var? Üstte KPI şeridi, ortada liste, sağda panel, altta sekme? 2. **Ana bölge ne?** Tablo mu, kart ızgarası mı, pano mu, takvim mi, form mu? 3. **Araç çubuğu** — hangi butonlar var, ne yazıyorlar, ikonları ne? 4. **Sütunlar** — başlıklar, hizalama, biçim (para, tarih, yüzde), rozet/renkli durum hücreleri. 5. **Filtre/arama** — başlık altı filtre satırı, arama kutusu, tarih aralığı seçicileri. 6. **Form alanları** — kontrol tipleri ve kolon düzeni (kaç kolon, hangi alan kaç sütun kaplıyor). 7. **Satır aksiyonları** — satır sonundaki ikonlar. 8. **Durum göstergeleri** — onay adımları, ilerleme çubukları, etiketler. #### 0.7.2 Görsel öğe → artefakt eşlemesi | Görselde gördüğün | Karşılığı | | --- | --- | | Başlık satırı + veri satırları olan tablo | Grid layout | | Kart ızgarası | Card layout | | Dikey kolonlar + sürüklenebilir kartlar | Todo (Kanban) layout · `TodoOptionDto.StatusExpr` | | Ay/hafta takvimi, saat çizelgesi | Scheduler layout | | Yatay zaman çubukları, bağımlılık okları | Gantt layout | | Girintili ağaç, açılır düğümler | Tree layout | | Satır/sütun kesişimli özet tablo, ara toplamlar | Pivot layout | | Çubuk/çizgi/pasta grafik | Chart layout · `SeriesJson` | | Üstte sayı gösteren küçük kutular | `Widgets` (`WidgetEditDto`) | | Kaydın altında sekmeler (Kalemler, Ekler, Geçmiş) | `SubForms` | | Araç çubuğundaki özel buton | `CommandColumnJson` · `ButtonPosition: 1` | | Satır sonundaki ikon | `CommandColumnJson` · `ButtonPosition: 0` | | Başlık altındaki filtre satırı | `FilterRowJson` | | Sağ üstteki arama kutusu | `SearchPanelJson` | | "Sürükleyip gruplayın" şeridi | `GroupPanelJson` | | Üstte ek filtre çubuğu (tarih aralığı, şube seçimi) | `ExtraFilterJson` | | Sayfalama şeridi | `PagerOptionJson` | | Renkli durum rozeti | `ColumnStylingJson` / `ColumnCssClass` | | Onayla / Reddet butonları | `WorkflowJson` (otomatik gelir, elle tanımlama) | | Yan yana bağımsız bölümler, karışık yerleşim | Custom Component · `FlexRow` + düğümler | #### 0.7.3 Görseldeki kontrol → `EditorType` | Görselde | `EditorType` | Ek | | --- | --- | --- | | Tek satır metin kutusu | `dxTextBox` | | | Çok satırlı kutu | `dxTextArea` | `autoResizeEnabled` | | Sağında ok olan açılır liste | `dxSelectBox` | lookup zorunlu | | İçinde etiket/çip olan açılır liste | `dxTagBox` | lookup zorunlu | | Büyüteçli seçim + tablo açılıyor | `dxGridBox` / `dxLookup` | `columns` | | Onay kutusu | `dxCheckBox` | | | Aç/kapa anahtarı | `dxSwitch` | `switchedOnText` | | Yan yana yuvarlak seçenekler | `dxRadioGroup` | `layout: horizontal` | | Takvim ikonlu alan | `dxDateBox` | | | İki tarihli aralık alanı | `dxDateRangeBox` | | | Artı/eksi oklu sayı | `dxNumberBox` | `format.precision` | | `₺ 1.250,00` biçimli alan | `dxNumberBox` | `format.type: currency` | | `%` biçimli alan | `dxNumberBox` | `format.type: percent` | | Renk kutucuğu | `dxColorBox` | | | Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` | | Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` | | Görsel küçük resmi / yükleme alanı | `dxImageViewer` / `dxImageUpload` | `accept`, `maxFileSize` | | Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | | | Kırmızı yıldızlı etiket | `IsRequired: true` | | #### 0.7.4 Görselden okunamayacak şeyler Bunlar bir ekran görüntüsünde **görünmez**; varsayımını yaz ve bildir: | Bilgi | Varsayılan davranış | | --- | --- | | Tablo/kolon adları | Görünen başlıklardan PascalCase kolon adı türet, tabloyu `sql/object/` altında sen üret | | Anahtar alan ve tipi | `Id` / `UNIQUEIDENTIFIER` (`KeyFieldDbSourceType: 9`) | | Lookup'ların kaynağı | Az sayıda sabit seçenek görünüyorsa `StaticData`, kod/isim çifti gerektiren yerlerde `Query` | | Tenant/şube kırılımı | `IsTenant: true` (§CLAUDE varsayılanı) | | Yetki grubu ve menü kökü | Ekranın ait olduğu modülden türet, kullanıcıya sor değil bildir | | Dil metinleri | Görseldeki dilde yaz, diğer dile çevir; ikisi de doldurulur | | Onaycılar | Yer tutucu bırak (``) ve kullanıcıya sor — **bu, sorulması gereken bir şeydir** | | İş kuralları (hesap, koşul) | Görselde formül görünmüyorsa `EditorScript` yazma; sor | #### 0.7.5 Görsel için de aynı protokol Görselden çıkardığın tarifi §0.6.1'deki tabloya sok, yolu seç, §0.2'deki sırayla dosyaları üret. Görsel bir "tasarım sözleşmesi" değildir: platformun kendi bileşenleriyle **en yakın** karşılığı üretilir, piksel taklidi yapılmaz. Görseldeki bir öğenin platformda karşılığı yoksa bunu üretim sonunda açıkça yaz. --- ## 1. Hangi istek → hangi artefakt | İstek | Üretilecek artefakt | Bölüm | | --- | --- | --- | | "X tablosunun listesi/ekranı olsun" | Wizard seed dosyası (`ComponentKind: 0`) | 4 | | "Şu alanı otomatik hesaplasın / şarta göre kilitlensin" | `EditorScript` (Script Builder tarifi) | 5 | | "Alan şu formatta / maskeyle görünsün" | `EditorOptions` (Options Builder) | 6 | | "Ekran bir tablodan değil, birleştirilmiş veriden beslensin" | SQL View Designer → `View` + Wizard | 8 | | "Serbest yerleşimli bir sayfa / dashboard / özel form" | Custom Component + Wizard (`ComponentKind: 1`) | 7 | | "Bir tabloya REST API açalım" | CRUD Endpoint | 9 | | "Elle yazdığım bir SQL'i API olarak açalım" | Custom Endpoint | 9.2 | | "API'de iş mantığı/hesap/entegrasyon lazım" | Dynamic Service | 9.3 | | "Onay süreci olsun" | `WorkflowDto` + workflow kriterleri | 4.7 | | "Ana-detay ekranı" | `SubForms` | 4.6 | | "Üstte KPI kartları" | `Widgets` | 4.6 | | Hiçbiri yetmiyorsa | Dynamic Service → en son çare kod | `ai.instructions.md` §4 | --- ## 2. İsimlendirme sözleşmesi Tutarlılık zorunludur; menü, route, yetki ve dil anahtarı aynı kökten türer. | Şey | Kalıp | Örnek | | --- | --- | --- | | Tablo | `{Modul}_T_{Entity}` | `Mrp_T_Order` | | View | `{Modul}_V_{Ad}` | `Mrp_V_OrderSummary` | | `ListFormCode` | `App.{Modul}.{Liste}` | `App.Mrp.Orders` | | `MenuCode` | `ListFormCode` ile aynı | `App.Mrp.Orders` | | Üst menü kodu | `App.{Modul}` | `App.Mrp` | | Yetki grubu | `App.{Modul}` | `App.Mrp` | | Yetkiler | `{MenuCode}` + `.Create/.Update/.Delete/.Export/.Import/.Note` | `App.Mrp.Orders.Create` | | Dil anahtarı (menü/başlık) | `{MenuCode}` | `App.Mrp.Orders` | | Dil anahtarı (alan) | `App.Listform.ListformField.{Alan}` | `App.Listform.ListformField.OrderNo` | | Route (List) | `/admin/list/{ListFormCode}` — statik `:listFormCode` route'u karşılar, DB'ye kayıt yazılmaz | `/admin/list/App.Mrp.Orders` | | Route (Custom) | Bileşenin `RoutePath` değeri; route doğrudan bundan üretilir | `/admin/order-board` | | Wizard seed dosyası | `{yyyyMMddHHmmss}_{WizardName}.json` | `20260826101500_Orders.json` | | Custom component seed | `{Name}.json` | `OrderBoard.json` | | CRUD endpoint seed | `{EntityName}.json` | `Mrp_T_Order.json` | Kapsam klasörü: host bağlamında `configs/seeds/host/…`, tenant bağlamında `configs/seeds/tenants/{tenantId}/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır; yol elle kurulmaz. --- ## 3. Ortak enum değerleri (seed dosyalarına **sayı** olarak yazılır) **`ComponentKind`** — `0` List · `1` Custom **`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query · `5` StoredProcedure **`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService **`DbSourceType` / `KeyFieldDbSourceType`** (`System.Data.DbType`): | # | Ad | SQL karşılığı | | --- | --- | --- | | 1 | Binary | `binary`, `varbinary`, `image` | | 2 | Byte | `tinyint` | | 3 | Boolean | `bit` | | 5 | Date | `date` | | 6 | DateTime | `datetime`, `datetime2`, `smalldatetime` | | 7 | Decimal | `decimal`, `numeric`, `money` | | 8 | Double | `float`, `real` | | 9 | Guid | `uniqueidentifier` | | 10 | Int16 | `smallint` | | 11 | Int32 | `int` | | 12 | Int64 | `bigint` | | 16 | String | `nvarchar`, `varchar`, `nchar`, `char`, `text` | | 17 | Time | `time` | | 25 | Xml | `xml` | | 27 | DateTimeOffset | `datetimeoffset` | **Editör tipleri (`EditorType`)** — 21 tip. DevExtreme'den gelenler: `dxAutocomplete`, `dxCalendar`, `dxCheckBox`, `dxColorBox`, `dxDateBox`, `dxDateRangeBox`, `dxDropDownBox`, `dxHtmlEditor`, `dxLookup`, `dxNumberBox`, `dxRadioGroup`, `dxRangeSlider`, `dxSelectBox`, `dxSlider`, `dxSwitch`, `dxTagBox`, `dxTextArea`, `dxTextBox`. Platformun kendi editörleri (`PlatformEditorTypes`): `dxGridBox`, `dxImageViewer`, `dxImageUpload` (+ `dxTagBox` platform tarafında da özel işlenir). Tip seçimi kuralı: `Boolean → dxCheckBox`/`dxSwitch`, tarih → `dxDateBox`, sayı → `dxNumberBox`, FK/lookup → `dxSelectBox` (az kayıt) veya `dxGridBox`/`dxLookup` (çok kayıt/çok kolon), çok değerli FK → `dxTagBox`, uzun metin → `dxTextArea`, HTML → `dxHtmlEditor`. Her editörün kendine özgü seçenekleri için §4.3.2. --- ## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası) Konum: `configs/seeds/{kapsam}/wizard/{yyyyMMddHHmmss}_{WizardName}.json` Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` ### 4.1 Dosya iskeleti ```jsonc { "Wizard": { /* ListFormWizardDto — 4.2 */ }, "IsDeletedField": true, // tabloda IsDeleted var mı (soft delete) "IsCreatedField": true, // tabloda CreatorId var mı (audit) "InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar "LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [], "MenuCodes": [], "DataSourceCodes": [] } } ``` `InsertedRecords` **silme sözleşmesidir**: wizard silindiğinde yalnızca burada listelenen kayıtlar silinir, paylaşılan kayıtlara (var olan izin grubu, var olan üst menü) dokunulmaz. Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür. ### 4.2 `Wizard` bloğu — alan alan **Kimlik ve yol** | Alan | Anlam | | --- | --- | | `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. | | `WizardName` | Dosya adının ve export zip adının kaynağı. | | `ListFormCode` | Ekranın kodu; route ve metadata bu koda göre çözülür. | | `MenuCode` | Menü kaydının kodu; yetki adlarının kökü de budur. | | `MenuUrl` | List'te `/admin/list/{MenuCode}` hesaplanır; Custom'da bileşenin `RoutePath`'i yazılır. | | `CustomComponentName` | Yalnızca Custom yolunda; bağlanacak bileşenin adı. | | `EditFileName` | Yalnızca **istek girdisi**: doluysa güncellemedir, sunucu önce eski dosyayı ve ürettiği kayıtları siler. Seed dosyasına yazılmaz. | **Menü** | Alan | Anlam | | --- | --- | | `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true`. | | `MenuParentCode` / `MenuParentShortName` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. | | `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). | | `MenuOrder` | Kardeşler arası sıra. | **Yetki ve dil** | Alan | Anlam | | --- | --- | | `PermissionGroupName` | Var olan grup ya da yeni grup adı. | | `PermissionGroupDisplayNameEn` / `Tr` | Grup adıyla aynı dil anahtarına yazılır. Var olan grup seçilip boş bırakılırsa sunucu veritabanındaki değeri geri yazar. | | `LanguageTextMenuEn` / `Tr` | Menü etiketi. | | `LanguageTextTitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). | | `LanguageTextDescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). | | `LanguageTextMenuParentEn` / `Tr` | Üst menü etiketi. | Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export .Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hepsi admin rolüne grant edilir. **Veri** | Alan | Anlam | | --- | --- | | `DataSourceCode` | Bağlantı kodu; varsayılan `"Default"`. | | `DataSourceConnectionString` | Yeni bir bağlantı tanımlanıyorsa; aksi hâlde boş. **Asla gerçek kimlik bilgisi yazma.** | | `SelectCommandType` | Bkz. §3. | | `SelectCommand` | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. | | `KeyFieldName` / `KeyFieldDbSourceType` | Birincil anahtar ve tipi. | **Davranış** | Alan | Anlam | | --- | --- | | `IsTenant` / `IsBranch` / `IsOrganizationUnit` | Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. | | `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `AllowDetail` / `ConfirmDelete` | Toolbar ve satır aksiyonları. | | `DefaultLayout` | `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. | | `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | Hangi görünümlerin açık olduğu. Açtığın her görünümün option bloğunu da doldur. | **Görünüm option blokları** — yalnızca ilgili bayrak `true` ise anlamlıdır: | Blok | Zorunlu alanlar | | --- | --- | | `TreeOptionDto` | `KeyExpr`, `ParentIdExpr`; opsiyonel `HasItemsExpr`, `RootValue`, `AutoExpandAll`, `RecursiveSelection` | | `GanttOptionDto` | `KeyExpr`, `ParentIdExpr`, `TitleExpr`, `StartExpr`, `EndExpr`; `ProgressExpr`, `ScaleType` (`hours`/`days`/`weeks`/`months`), `Allow*` bayrakları | | `SchedulerOptionDto` | `TextExpr`, `StartDateExpr`, `EndDateExpr`; `AllDayExpr`, `RecurrenceRuleExpr`, `StartDayHour`, `EndDayHour`, `DefaultView` (`day`/`week`/`month`), `CellDuration`, `FirstDayOfWeek`, `Allow*` | | `TodoOptionDto` | `TitleExpr`, `StatusExpr`; `DescriptionExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `StatusOrder` (virgülle ayrılmış kolon sırası), `AllowDragging` | ### 4.3 `Groups` — alanlar ve düzenleme formu `Groups`, hem grid sütunlarını hem düzenleme formunun düzenini tanımlar. Buradaki her `Items` öğesi bire bir bir **`ListFormField` kaydına** dönüşür (§4.8); ekranda görünen sütunların tek kaynağı odur. Grup yapısı yalnızca düzenleme formunun yerleşimini belirler — grid sütun sırası `Items` sırasından gelir. ```jsonc "Groups": [ { "Caption": "Genel", "ColCount": 2, "Items": [ /* WizardColumnItemInputDto */ ] } ] ``` Her `Items` öğesi: | Alan | Anlam | | --- | --- | | `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). | | `CaptionName` | Dil anahtarı ya da düz başlık. | | `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. | | `EditorType` | Bkz. §3. | | `EditorOptions` | DevExtreme editör JSON'u — §6. | | `EditorScript` | Alan davranış script'i — §5. | | `DbSourceType` | Bkz. §3. | | `IsRequired` | Zorunluluk. | | `IncludeInEditingForm` | `false` ise yalnızca grid sütunu olur, forma girmez. | | `ColSpan` | Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). | | `LookupDataSourceType` | `1/2/3` — bkz. §3. `0` = lookup yok. | | `LookupQuery` | `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. | | `ValueExpr` / `DisplayExpr` | Lookup'ın değer ve etiket kolonları. | ### 4.3.1 Sütunlar nasıl otomatik üretilir Gösterilecek **tüm sütunlar `ListFormField` kayıtlarında** durur. Wizard bu kayıtları veritabanı kolon metadata'sından (`GetTableColumnsAsync`) otomatik üretir; sen yalnızca farklı olmasını istediğin şeyi belirtirsin. Bir kolon `Groups[].Items[]` içine eklendiğinde varsayılanlar şöyle türetilir: | Alan | Nereden gelir | | --- | --- | | `EditorType` | SQL tipinden çıkarılır (aşağıdaki tablo) | | `DbSourceType` | SQL tipinden `System.Data.DbType` karşılığına eşlenir (§3) | | `IsRequired` | Kolon `NOT NULL` ise `true` | | `TurkishCaption` / `EnglishCaption` | Kolon adı PascalCase'ten kelimelere ayrılır: `OrderNo` → `Order No` | | `CaptionName` | `App.Listform.ListformField.{KolonAdı}` | | `ColSpan` | `1` | | `IncludeInEditingForm` | `true` | | `ValueExpr` / `DisplayExpr` | `Key` / `Name` (lookup tanımlanırsa değiştirilir) | | `EditorOptions` / `EditorScript` | Boş — ihtiyaç varsa sen yazarsın | **SQL tipi → `EditorType` çıkarımı** (`inferEditorType`): | SQL tipi | Üretilen editör | | --- | --- | | `bit` | `dxCheckBox` | | `int`, `bigint`, `smallint`, `tinyint`, `decimal`, `numeric`, `float`, `real`, `money`, `smallmoney` | `dxNumberBox` | | `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset` | `dxDateBox` | | diğer hepsi | `dxTextBox` | Çıkarım kasıtlı olarak dardır. **Foreign key kolonları `dxNumberBox`/`dxTextBox` olarak gelir** — bunları `dxSelectBox` / `dxGridBox` / `dxTagBox` yapmak ve lookup tanımlamak senin işindir. Uzun metin (`nvarchar(max)`) için `dxTextArea`, HTML içerik için `dxHtmlEditor`, durum kolonları için `dxRadioGroup`/`dxSwitch` seçilmelidir; otomatik çıkarım bunları bilemez. ### 4.3.2 Editör tipine göre özellikler Her editör kendi `EditorOptions` sözlüğüne sahiptir. Aşağıdaki tablo, o editöre **özgü** olan anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir. | `EditorType` | Ne için | Uygun `DbSourceType` | Kendine özgü `EditorOptions` | | --- | --- | --- | --- | | `dxTextBox` | Kısa metin | String (16) | — (ortak: `maxLength`, `mask`, `placeholder`) | | `dxTextArea` | Uzun metin | String (16) | `autoResizeEnabled`, `minHeight` | | `dxNumberBox` | Sayı | Int32/Int64/Decimal/Double (7,8,10,11,12) | `format.type` (`fixedPoint`/`currency`/`percent`), `format.precision`, `format.currency`, `useMaskBehavior`, `useLargeSpinButtons`, `invalidValueMessage` | | `dxDateBox` | Tarih / tarih-saat | Date/DateTime (5,6,26,27) | `useMaskBehavior` (+ ortak: `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `calendarOptions.*`, `invalidDateMessage`) | | `dxDateRangeBox` | Tarih aralığı | Date/DateTime | ortak tarih anahtarları | | `dxCalendar` | Gömülü takvim | Date/DateTime | `calendarOptions.zoomLevel`, `calendarOptions.firstDayOfWeek` | | `dxCheckBox` | Evet/hayır | Boolean (3) | `text` (kutunun yanındaki etiket) | | `dxSwitch` | Evet/hayır (anahtar) | Boolean (3) | `switchedOnText`, `switchedOffText` | | `dxRadioGroup` | Az sayıda seçenek | String/Int32 | `layout` (`horizontal`/`vertical`) + **lookup zorunlu** | | `dxSelectBox` | Tek seçim (az kayıt) | FK tipi | `acceptCustomValue`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `noDataText`, `showDataBeforeSearch` + **lookup zorunlu** | | `dxLookup` | Tek seçim (çok kayıt, mobil dostu) | FK tipi | `dropDownOptions.*`, `searchEnabled`, `applyValueMode` + **lookup zorunlu** | | `dxDropDownBox` | Özel içerikli açılır kutu | FK tipi | `dropDownOptions.width/height/hideOnOutsideClick`, `deferRendering`, `openOnFieldClick` | | `dxGridBox` | Çok kolonlu seçim ızgarası | FK tipi | `columns`\*, `filterRowVisible`\*, `selectionMode`\*, `acceptCustomValue`\* + **lookup zorunlu** | | `dxTagBox` | Çok değerli seçim | String (virgüllü) / ilişki tablosu | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `searchEnabled`\*, `acceptCustomValue`\*, `hideSelectedItems`, `multiline` + **lookup zorunlu** | | `dxAutocomplete` | Serbest metin + öneri | String (16) | `minSearchLength`, `searchExpr`, `searchTimeout` | | `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` | | `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | | `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | | `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` | | `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | | `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | `\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür: `GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir sayı, `multiple` bir boolean olmalıdır. **Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` + `DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir: ```jsonc { "FieldName": "StatusId", "EditorType": "dxSelectBox", "DbSourceType": 11, "LookupDataSourceType": 1, "LookupQuery": "[{\"Key\":1,\"Name\":\"Taslak\"},{\"Key\":2,\"Name\":\"Onayda\"},{\"Key\":3,\"Name\":\"Onaylandı\"}]", "ValueExpr": "Key", "DisplayExpr": "Name" } ``` `LookupDataSourceType` `1` (StaticData) ise `LookupQuery` bir JSON dizisi, `2` (Query) ise SQL, `3` (WebService) ise URL'dir. Cascade (ebeveyn–çocuk) davranışı ListForm editöründeki lookup ayarlarından tanımlanır. ### 4.3.3 Her editörde geçerli ortak seçenekler Gruplar hâlinde (`optionSpecs.ts` sözlüğünün tamamı UI'yı buradan üretir): | Grup | Anahtarlar | | --- | --- | | Genel | `disabled`, `readOnly`, `visible`, `hint`, `tabIndex`, `showClearButton`, `valueChangeEvent`, `validationMessageMode`, `validationMessagePosition` | | Görünüm | `width`, `height`, `stylingMode`, `label`, `labelMode`, `elementAttr.class`, `inputAttr.style`, `inputAttr.aria-label`, `buttons` | | Metin | `placeholder`, `maxLength`, `spellcheck`, `mode`, `mask`, `maskChar`, `maskRules.X`, `maskInvalidMessage`, `showMaskMode`, `useMaskedValue`, `encodeHtml` | | Sayı | `min`, `max`, `format`, `showSpinButtons` | | Tarih | `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `invalidDateMessage` | | Açılır liste | `noDataText`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `showDataBeforeSearch`, `openOnFieldClick`, `deferRendering`, `wrapItemText`, `dropDownOptions.*` | Elle JSON yazmak yerine C# tarafında `EditorOptions` akıcı yapısını kullan (§6); hazır başlangıçlar bu anahtarların doğru bileşimlerini üretir. ### 4.4 Çalışan örnek — tablo üzerinden liste ekranı ```jsonc { "Wizard": { "ComponentKind": 0, "WizardName": "Orders", "ListFormCode": "App.Mrp.Orders", "MenuCode": "App.Mrp.Orders", "MenuOrder": 1, "CreateMenu": true, "MenuUrl": "/admin/list/App.Mrp.Orders", "IsTenant": true, "IsBranch": false, "IsOrganizationUnit": false, "AllowAdding": true, "AllowUpdating": true, "AllowDeleting": true, "AllowDetail": false, "ConfirmDelete": true, "DefaultLayout": "grid", "Grid": true, "Card": true, "Pivot": true, "Chart": true, "Tree": false, "Gantt": false, "Scheduler": false, "Todo": false, "LanguageTextMenuEn": "Orders", "LanguageTextMenuTr": "Siparişler", "LanguageTextTitleEn": "Orders", "LanguageTextTitleTr": "Siparişler", "LanguageTextDescEn": "Order list", "LanguageTextDescTr": "Sipariş listesi", "LanguageTextMenuParentEn": "MRP", "LanguageTextMenuParentTr": "MRP", "PermissionGroupName": "App.Mrp", "PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP", "MenuParentCode": "App.Mrp", "MenuParentShortName": "MRP", "MenuParentIcon": "FcFactory", "MenuIcon": "FcShipped", "DataSourceCode": "Default", "DataSourceConnectionString": "", "SelectCommandType": 1, "SelectCommand": "Mrp_T_Order", "KeyFieldName": "Id", "KeyFieldDbSourceType": 11, "Groups": [ { "Caption": "Genel", "ColCount": 2, "Items": [ { "FieldName": "OrderNo", "CaptionName": "App.Listform.ListformField.OrderNo", "TurkishCaption": "Sipariş No", "EnglishCaption": "Order No", "EditorType": "dxTextBox", "DbSourceType": 16, "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, "EditorOptions": "{\"showClearButton\":true,\"maxLength\":20}" }, { "FieldName": "CustomerId", "CaptionName": "App.Listform.ListformField.Customer", "TurkishCaption": "Cari", "EnglishCaption": "Customer", "EditorType": "dxSelectBox", "DbSourceType": 11, "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, "LookupDataSourceType": 2, "LookupQuery": "SELECT Id, Name FROM Crm_T_Customer WHERE IsDeleted = 0", "ValueExpr": "Id", "DisplayExpr": "Name" }, { "FieldName": "Quantity", "CaptionName": "App.Listform.ListformField.Quantity", "TurkishCaption": "Miktar", "EnglishCaption": "Quantity", "EditorType": "dxNumberBox", "DbSourceType": 7, "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2},\"showSpinButtons\":true}" }, { "FieldName": "UnitPrice", "CaptionName": "App.Listform.ListformField.UnitPrice", "TurkishCaption": "Birim Fiyat", "EnglishCaption": "Unit Price", "EditorType": "dxNumberBox", "DbSourceType": 7, "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}" }, { "FieldName": "Total", "CaptionName": "App.Listform.ListformField.Total", "TurkishCaption": "Tutar", "EnglishCaption": "Total", "EditorType": "dxNumberBox", "DbSourceType": 7, "IsRequired": false, "IncludeInEditingForm": true, "ColSpan": 2, "EditorOptions": "{\"readOnly\":true,\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}", "EditorScript": "// @builder {\"v\":1,\"rules\":[{\"id\":\"r1\",\"recipe\":\"multiply\",\"condition\":{\"operator\":\"always\",\"source\":\"\",\"value\":\"\"},\"params\":{\"left\":\"Quantity\",\"right\":\"UnitPrice\",\"target\":\"Total\",\"digits\":\"2\"}}]}\nset('Total', round(num('Quantity') * num('UnitPrice'), 2))" } ] } ], "SubForms": [], "Widgets": [], "WorkflowDto": { "ApprovalUserFieldName": "", "ApprovalDateFieldName": "", "ApprovalStatusFieldName": "", "ApprovalDescriptionFieldName": "", "ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false, "Criteria": [] } }, "IsDeletedField": true, "IsCreatedField": true, "InsertedRecords": { "LanguageKeys": ["App.Mrp.Orders", "App.Mrp"], "PermissionGroupNames": ["App.Mrp"], "PermissionNames": ["App.Mrp.Orders", "App.Mrp.Orders.Create", "App.Mrp.Orders.Update", "App.Mrp.Orders.Delete", "App.Mrp.Orders.Export", "App.Mrp.Orders.Import", "App.Mrp.Orders.Note"], "MenuCodes": ["App.Mrp", "App.Mrp.Orders"], "DataSourceCodes": [] } } ``` > `EditorScript` alanındaki `// @builder` başlığı ile onu izleyen kod satırı **birebir > tutarlı** olmalıdır. Elle yazmak yerine C# tarafında `EditorScript.Build(...)` kullan ya da > Script Builder'da üret; uyuşmazsa dialog script'i "elle düzenlenmiş" sayar. ### 4.4.1 Seeder'ın senin yerine ürettikleri — bunları dosyaya yazma `WizardDataSeeder`, dosyadaki `Wizard` bloğundan yola çıkarak aşağıdakileri **kendisi** üretir. Bunları seed dosyasında aramaya kalkma, orada yokturlar: | Üretilen | Kural | | --- | --- | | Dil anahtarları | `{MenuCode}` (menü), `{ListFormCode}.Title`, `{ListFormCode}.Desc`, `{PermissionGroupName}` ve her alan için `CaptionName` | | Yetkiler | `{MenuCode}` kökü + `.Create/.Update/.Delete` (+ List yolunda `.Export/.Import/.Note`) | | Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` | | Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` | | Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` | | `DataSource` | `DataSourceCode` yoksa yaratılır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` | | `LayoutJson` | Görünüm bayrakları + `DefaultLayout` | | `PermissionJson` / alan `PermissionJson` | `{MenuCode}` kökünden türetilir | | `EditingFormJson` | `Groups` içinden; anahtar alan ve `IncludeInEditingForm: false` olanlar hariç | | Form genişlik/yükseklik | Grup kolon sayısı ve satır sayısından hesaplanır (en fazla 3 kolon) | | `FilterRowJson`, `HeaderFilterJson`, `SearchPanelJson`, `GroupPanelJson`, `ColumnOptionJson`, `PagerOptionJson`, `ExportJson` | Varsayılanlar | | `DeleteCommand` | `IsDeletedField: true` ise soft delete `UPDATE` komutu; `false` ise `null` | | `DefaultFilter` | `IsDeletedField: true` ise `"IsDeleted" = 'false'` | | `InsertFieldsDefaultValueJson` | `IsCreatedField: true` ise `CreationTime=@NOW`, `CreatorId=@USERID`, `IsDeleted=false`, `Id=@NEWID`; değilse yalnızca `Id=@NEWID` | | `DeleteFieldsDefaultValueJson` | `IsDeletedField: true` ise `DeleterId=@USERID`, `Id=@ID`; değilse yalnızca `Id=@ID` | | `ValidationRuleJson` | `IsRequired: true` olan alanlara required kuralı | | `LookupJson` | `LookupQuery` doluysa `LookupDataSourceType` + `DisplayExpr` + `ValueExpr` ile | | `ShowNote` | Alt form veya iş akışı varsa açılır | | `SelectionJson` | İş akışı varsa `single`, yoksa `none` | | Görünüm option JSON'ları | İlgili bayrak açık **ve** zorunlu alanı dolu ise yazılır; eksikse görünüm sessizce kapanır | **Dikkat edilecek tuzaklar:** - `Tree`/`Gantt` için `ParentIdExpr`, `Scheduler` için `TextExpr`, `Todo` için `TitleExpr` **ve** `StatusExpr` boşsa o görünümün JSON'u hiç yazılmaz — bayrağı `true` yapmak yetmez. - Anahtar alanı (`KeyFieldName`) `Groups` içine koyarsan sütun olarak gizlenir ve forma girmez; yine de tanımlaman doğrudur, çünkü tip bilgisi oradan okunur. - `IsDeletedField: false` verirsen ekranda **kalıcı silme** olmaz (`DeleteCommand` null kalır); soft delete kolonları olmayan tabloda silme istiyorsan ListForm editöründen komut yazmalısın. - Dil anahtarı zaten varsa metni **ezilmez**. Yanlış metin görüyorsan anahtar başka bir yerde tanımlanmıştır. ### 4.5 Güncelleme, idempotency ve silme semantiği **Seeder idempotenttir — bir ekran zaten varsa dosya atlanır.** "Zaten var" ölçütü yola göre değişir: | Yol | Kontrol | | --- | --- | | `List` | `ListForm` kaydında bu `ListFormCode` var mı | | `Custom` + menülü | Bu `MenuCode` ile menü var mı | | `Custom` + menüsüz | Bu `MenuCode` adında yetki var mı | Sonuç: **var olan bir ekranı seed dosyasını düzenleyerek değiştiremezsin.** Değiştirmenin üç yolu vardır: 1. Wizard ekranından `EditFileName` ile yeniden çalıştır (önce eski dosya ve `InsertedRecords` kayıtları silinir, sonra yenisi üretilir) — **tercih edilen yol**. 2. ListForm editöründen ince ayar yap (seed dosyasına yansımaz, §9.6 uyarısı). 3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır. > `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow` > kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir. **Silme:** yalnızca `InsertedRecords` içindekiler silinir. Paylaşılan üst menü ve izin grubu başka bir wizard tarafından yaratıldıysa kalır. Bu yüzden `InsertedRecords` listesine var olan bir kaydı yazmak veri kaybettirir. ### 4.6 Alt formlar ve widget'lar **`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar: ```jsonc "SubForms": [ { "TabType": "List", // List | Tree | Gantt | Scheduler | Form | Chart "TabTitle": "App.Mrp.OrderItems", // dil anahtarı "Code": "App.Mrp.OrderItems", // hedef ListFormCode "IsRefresh": true, // ana kayıt değişince yenilensin "Relation": [ { "ParentFieldName": "Id", "ChildFieldName": "OrderId", "DbType": 11 } ] } ] ``` `Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir. **`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları: ```jsonc "Widgets": [ { "Title": "App.Mrp.Widget.OpenOrders", "SubTitle": "App.Mrp.Widget.OpenOrdersSub", "SqlQuery": "SELECT COUNT(*) FROM Mrp_T_Order WHERE IsDeleted = 'false' AND Status = 1", "Value": "", // sabit değer; SqlQuery doluysa boş bırakılır "ValueClassName": "text-3xl", "Icon": "FcClock", "Color": "blue", "ColSpan": 1, "ColGap": 16, "ClassName": "", "OnClick": "", // tıklanınca açılacak adres "IsActive": true } ] ``` `SqlQuery` **tek hücre** döndürmelidir; tenant kırılımı gerekiyorsa sorguya `TenantId` koşulunu kendin yaz — widget sorgusu ListForm'un otomatik tenant filtresine tabi değildir. Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde görünmesin ama kendi yetkileri ve alan tanımları olsun. Dosya adının zaman damgası ana wizard'ınkinden **küçük** olmalıdır (§0.2). ### 4.7 İş akışı (onay süreci) `WorkflowDto` onay alanlarını tabloda karşılığı olan kolonlara bağlar: | Alan | Tabloda karşılığı | | --- | --- | | `ApprovalUserFieldName` | Onaylayan kullanıcı | | `ApprovalDateFieldName` | Onay tarihi | | `ApprovalStatusFieldName` | Durum | | `ApprovalDescriptionFieldName` | Onay/ret açıklaması | | `ApprovalIsFilterUserName` | Liste yalnızca kullanıcının onayına düşenleri göstersin | | `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün | `Criteria` düğüm grafiğidir. `Kind` değerleri: **`Start`**, **`Compare`**, **`Approval`**, **`Inform`**, **`End`**. ```jsonc "WorkflowDto": { "ApprovalUserFieldName": "ApproverId", "ApprovalDateFieldName": "ApprovalDate", "ApprovalStatusFieldName": "Status", "ApprovalDescriptionFieldName": "ApprovalNote", "ApprovalIsFilterUserName": true, "ApprovalIsResetWorkflow": true, "Criteria": [ { "Id": "N1", "Kind": "Start", "Title": "Başlangıç", "NextOnStart": "N2", "PositionX": 40, "PositionY": 40 }, { "Id": "N2", "Kind": "Compare", "Title": "Tutar kontrolü", "CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000, "CompareOutcomes": [ { "Label": "> 5000", "TargetId": "N3", "Conditions": [ { "CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000 } ] }, { "Label": "<= 5000", "TargetId": "N4", "Conditions": [ { "CompareColumn": "Amount", "CompareOperator": "<=", "CompareValue": 5000 } ] } ], "PositionX": 40, "PositionY": 160 }, { "Id": "N3", "Kind": "Approval", "Title": "Genel Müdür onayı", "Approver": "", "NextOnApprove": "N5", "NextOnReject": "N6", "PositionX": 300, "PositionY": 260 }, { "Id": "N4", "Kind": "Approval", "Title": "Yönetici onayı", "Approver": "", "NextOnApprove": "N5", "NextOnReject": "N6", "PositionX": 40, "PositionY": 260 }, { "Id": "N5", "Kind": "Inform", "Title": "Talep sahibine bilgi", "Approver": "", "NextOnStart": "N7", "PositionX": 170, "PositionY": 380 }, { "Id": "N6", "Kind": "End", "Title": "Reddedildi", "PositionX": 340, "PositionY": 480 }, { "Id": "N7", "Kind": "End", "Title": "Onaylandı", "PositionX": 170, "PositionY": 480 } ] } ``` Kurallar: - **Id sözleşmesi:** `N` ile başlayan kısa id'ler seeder tarafından `{ListFormCode}-{Id}` hâline getirilir; hedef alanlar (`NextOn*`, `CompareOutcomes[].TargetId`) da aynı normalizasyondan geçer. Bu yüzden dosyada `N1`, `N2` yazman yeterlidir ve **tekil olmaları şarttır**. - `Compare` düğümünde çok dallı karar için `CompareOutcomes` kullanılır; iki dallı basit karar için `NextOnTrue` / `NextOnFalse` yeterlidir. - `Approval` düğümü `NextOnApprove` / `NextOnReject`, `Start` ve `Inform` düğümleri `NextOnStart` alanını kullanır. `End` düğümünün çıkışı yoktur. - `CompareValue` **sayısaldır** (`decimal`); metin karşılaştırması bu düğümle yapılmaz. - `Title` tekil olmalıdır; seeder tekrar eden başlıkları ayrıştırır ama okunabilirliği bozar. - Akış varsa ListForm `SelectionMode = single` olur ve not paneli otomatik açılır. - Görsel tasarım `/admin/listform/edit/{kod}` → Workflow sekmesindedir. ### 4.8 `ListForm` entity — List component'in tek kaynağı Bir List component'in **bütün** davranışı `ListForm` (ekran) ve `ListFormField` (alan) kayıtlarında durur. Wizard bunları üretir; `/admin/listform/edit/{kod}` düzenler. Prompt ile üretim yaparken wizard dosyası yazılır, ama hangi ayarın hangi kolona düştüğünü bilmek gerekir — çünkü wizard'ın kapsamadığı bir istek geldiğinde cevap "kod yazalım" değil, "şu JSON kolonunu şöyle ayarlayalım" olmalıdır. **Kimlik ve varyant** | Kolon | Ne yapar | | --- | --- | | `ListFormCode` | Ekranın kodu; kullanıcı/rol/dil özelleştirmeleri buna bağlanır | | `CultureName` · `UserId` · `RoleId` | **Aynı ekranın varyantları.** Aynı `ListFormCode` ile ikinci bir kayıt açıp yalnızca bir rol ya da dil için farklı sütun/düzen verebilirsin | | `ListFormType` | `List` · `Form` — ekranın türü | | `IsSubForm` / `SubFormsListFormType` | Alt form olarak açıldığında davranış | **Veri** | Kolon | Ne yapar | | --- | --- | | `DataSourceCode` | Bağlantı | | `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi | | `TableName` | Tablo adı/alias — yazma komutlarının hedefi | | `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar | | `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler | | `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) | | `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri | | `DataSourceJson` · `CommonJson` | Kaynak ve ortak ayarlar | **Yazma (CRUD)** | Kolon | Ne yapar | | --- | --- | | `InsertCommand` / `UpdateCommand` / `DeleteCommand` | Özel SQL komutları; boşsa platform üretir | | `*BeforeCommand` / `*AfterCommand` | Komut öncesi/sonrası çalışan SQL kancaları — **stok düşme, log yazma, durum güncelleme gibi işleri kod yazmadan burada yaparsın** | | `Insert/Update/DeleteFieldsDefaultValueJson` | Otomatik doldurulan alanlar (`@NOW`, `@USERID`, `@NEWID`, `@ID`) | | `Insert/Update/DeleteServiceAddress` | Varsayılan `list-form-data/*`; özel endpoint ya da Dynamic Service'e yönlendirilebilir | | `FormFieldsDefaultValueJson` | Form açılışındaki varsayılanlar | **Görünüm ve düzen — 8 layout** `LayoutJson` hangi layout'ların açık olduğunu ve varsayılanı taşır. Her layout'un kendi ayar kolonu vardır: | Layout | Ayar kolonu | Zorunlu alan | | --- | --- | --- | | **Grid** | `ColumnOptionJson`, `RowJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | — | | **Card** | `ColumnOptionJson` (kart alanları sütun tanımlarından türer) | — | | **Pivot** | `PivotOptionJson` + alan bazında `PivotSettingsJson` | — | | **Chart** | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `PanesJson`, `AnnotationsJson`, `CrosshairJson`, `ScrollBarJson`, `ZoomAndPanJson`, `SizeJson`, `MarginJson`, `TitleJson`, `AnimationJson`, `Common*Json` | `SeriesJson` | | **Tree** | `TreeOptionJson` | `KeyExpr` + `ParentIdExpr` | | **Gantt** | `GanttOptionJson` | `ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` | | **Scheduler** | `SchedulerOptionJson` | `TextExpr` + `StartDateExpr` + `EndDateExpr` | | **Todo (Kanban)** | `TodoOptionJson` | `TitleExpr` + `StatusExpr` | Sekiz layout **aynı `SelectCommand` üzerinden** beslenir; ayrı sorgu, ayrı ekran, ayrı menü gerekmez. Kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı saklanır. Bu, tek bir metadata kaydından DevExtreme'in `DataGrid`, `CardView`, `PivotGrid`, `Chart`, `TreeList`, `Gantt` ve `Scheduler` bileşenlerinin tamamının sürülmesi demektir. **Todo layout Kanban görünümüdür** ve diğerleriyle aynı veri hattından beslenir — kolonları `StatusExpr` değerlerinden üretir, kart sürüklendiğinde ilgili kolonu günceller. Yani "aynı veriyi pano olarak da görelim" isteği yeni bir ekran değil, `Todo: true` + `TodoOptionDto` demektir. **Filtre, arama, düzenleme** | Kolon | Ne yapar | | --- | --- | | `FilterRowJson` · `HeaderFilterJson` · `FilterPanelJson` · `SearchPanelJson` · `GroupPanelJson` | Filtreleme/arama/gruplama panelleri | | `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu | | `EditingOptionJson` | Düzenleme modu (row/cell/batch/form/popup), başlık, boyut, izinler | | `EditingFormJson` | Düzenleme formunun grup/sekme düzeni | | `CommandColumnJson` | Satır aksiyon sütunu | | `PermissionJson` | Ekran yetkileri (create/read/update/delete/export/import/note) | | `SubFormsJson` · `WidgetsJson` · `WorkflowJson` | Alt formlar, KPI kartları, onay akışı | | `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfa yüklenince çalışan JS/CSS — **son çare** | | `Width` · `Height` · `FullHeight` · `AdaptiveLayoutJson` | Boyut ve uyarlanabilir yerleşim | | `ExportJson` | xlsx/csv/pdf dışa aktarma | **`ListFormField` — alan başına ayarlar** | Grup | Kolonlar | | --- | --- | | Bağlama | `FieldName`, `SourceDbType`, `JoinTableJson` (kolon başka tablodan geliyorsa) | | Görünüm | `CaptionName`, `PlaceHolder`, `Visible`, `IsActive`, `Width`, `ListOrderNo`, `Alignment`, `Format`, `BandName` | | Sıralama/filtre | `SortIndex`, `SortDirection`, `AllowSearch`, `ColumnFilterJson`, `ColumnHeaderJson`, `GroupingJson`, `ColumnCustomizationJson` | | Özet | `TotalSummaryJson`, `GroupSummaryJson` | | Düzenleme | `EditorType2`, `EditorOptions`, `EditorScript`, `EditOrderNo`, `EditGroupOrderNo`, `ColSpan`, `AllowEditing`, `AllowAdding`, `ValidationRuleJson` | | Lookup | `LookupJson` | | Biçimlendirme | `ColumnStylingJson`, `ColumnCssClass`, `ColumnCssValue` | | Pivot | `PivotSettingsJson` | | Yetki | `PermissionJson` → `CanRead`, `CanCreate`, `CanUpdate`, `CanExport` | | Varyant | `UserId`, `RoleId`, `CultureName` | > **Kural:** "Şunu da yapabilir miyiz?" sorusuna cevap ararken önce bu iki tabloya bak. Aradığın > davranışın bir JSON kolonu varsa çözüm konfigürasyondur; `CustomJsSources` ve kod yazmak > ondan sonra gelir. ### 4.9 Toolbar ve satır butonları — `CommandColumnJson` 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: ```jsonc [ { "ButtonPosition": 1, // 0 = satır aksiyon sütunu · 1 = toolbar "AuthName": "App.Hr.AdvanceRequests.Update", // yetki yoksa buton hiç çizilmez "Text": "App.Hr.AdvanceRequests.SendToApproval", // dil anahtarı (başına :: eklenir) "Hint": "App.Hr.AdvanceRequests.SendToApprovalHint", "Icon": "check", "IsVisible": true, "VisibleExpression": "", "Url": "", "UrlTarget": "_blank", "DialogName": "", "DialogParameters": "", "OnClick": "" } ] ``` Bir butonun davranışı **üç moddan biridir** ve bu sırayla değerlendirilir: | Mod | Dolu alan | Ne yapar | | --- | --- | --- | | **Adres** | `Url` (+ `UrlTarget`) | Adresi açar. `@FieldName` token'ları **seçili ilk satırın** kolon değerleriyle değiştirilir (`/admin/form/App.Hr.AdvanceRequests/@Id`). PWA modunda hedef `_self` olur. | | **Diyalog** | `DialogName` + `DialogParameters` | Kayıtlı bir bileşeni diyalog olarak açar. `DialogParameters` bir JSON nesnesidir; `@Kolon` değerleri satırdan doldurulur (`{"requestId":"@Id","amount":"@Amount"}`). | | **Script** | `OnClick` | Serbest JS. Son çaredir; önce diğer iki mod denenir. | Kurallar: - `AuthName` **zorunlu sayılmalıdır**: yetki kontrolünden geçmeyen buton hiç çizilmez. Yetkisi olmayan kullanıcıya süreç butonu göstermek istemiyorsan tek gereken budur. - `Text` ve `Hint` dil anahtarıdır; çeviri `::` öneki eklenerek çözülür. Düz metin yazma. - Toolbar butonu **seçili satırlarla** çalışır; `SelectionJson` uygun modda (`single`/`multiple`) olmalıdır. İş akışı varsa wizard bunu zaten `single` yapar. - Satır bazlı aksiyon istiyorsan `ButtonPosition: 0` kullan; buton komut sütununda görünür. - Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir; onları `CommandColumnJson` ile tekrar tanımlama. Tipik süreç kalıbı: `Url` modunda bir Custom Endpoint'i ya da Dynamic Service'i çağıran adres verilir, dönüşte liste yenilenir. İş mantığı SQL/serviste kalır; ekranda yalnızca buton tanımı durur. --- ## 5. Artefakt B — `EditorScript` (alan davranışı) ### 5.1 Script metninin şekli ``` // @builder {"v":1,"rules":[ … ]} ← ilk satır: kuralların JSON'u // @runOnOpen ← yalnızca bir kural open/both ise ``` Dialog script'i regex ile çözmez; başlıktaki kurallardan script'i **yeniden üretip** metinle karşılaştırır. Aynı değilse script "elle düzenlenmiş" sayılır ve kural editörü kapanır. Bu yüzden başlık ile gövde asla ayrı ayrı düzenlenmez. ### 5.2 Tarifler (recipes) | Grup | `recipe` | Ürettiği | | --- | --- | --- | | calc | `multiply` | `set(T, round(num(A) * num(B), d))` | | calc | `subtract` | `set(T, round(num(A) - num(B), d))` | | calc | `percent` | oran alan ya da sabit; `mode` yok/`add`/`subtract` | | calc | `sum` | `set(T, round(sum('A','B',…), d))` | | calc | `formula` | serbest ifade: `set(T, num('Gross') * 0.18)` | | calc | `today` | `set(T, bugün + offsetDays)` (`yyyy-MM-dd`) | | calc | `days` | iki tarih arası gün (bitiş dahil) | | calc | `hours` | iki saat arası fark; bitiş küçükse ertesi güne taşar | | data | `copy` | seçili lookup kaydının kolonunu alana taşır | | data | `setValue` | sabit/şablonlu değer (`{Alan}`, `{value}`, `{selected.Yol}`) | | data | `clear` | verilen alanları boşaltır | | view | `readOnly` | koşula göre kilitler (`invert` ile tersi) | | interaction | `notify` | uyarı gösterir | | interaction | `ask` | onay ister; vazgeçilirse alan eski değerine döner ve script durur | | interaction | `openUrl` | adres açar | | integration | `apiToField` | API çağırır, cevabın bir parçasını alana yazar | | — | `custom` | serbest JS satırı (son çare) | Tetikleyici: `change` (varsayılan) · `open` (form açılışında varsayılan üretmek için) · `both`. Koşullar `and`/`or` ile birleşir: `always`, `equals`, `notEquals`, `contains`, `empty`, `notEmpty`, `greaterThan`, `lessThan`, `isTrue`, `isFalse`. ### 5.3 Runtime API (custom/formula içinde kullanılabilir) | İmza | Ne yapar | | --- | --- | | `value` / `field` | Değişen alanın değeri ve adı | | `get('Alan')` | Form verisinden okur (yol destekler) | | `num()` / `str()` / `bool()` / `dateOf()` | Tip dönüşümü | | `set('Alan', deger)` / `set({A:1,B:2})` | Yazar (toplu yazım tek flush) | | `clear('A','B')` | Boşaltır | | `copy('kolon','Alan')` | Seçili kayıttan taşır | | `selected('Yol')` / `snum()` / `sstr()` | Lookup/GridBox seçili kaydı | | `readOnly('Alan', kosul)` | Alanı kilitler | | `round(x, 2)` / `sum('A','B')` | Matematik | | `days('Bas','Bit')` / `hours('Bas','Bit')` | Tarih/saat farkı | | `tpl('{Alan} - {value}')` | Şablon | | `notify()` / `ask()` / `openUrl()` | Etkileşim | | `await api('/api/x','data.name')` | HTTP | | `isReady` | Script form açılışında mı çalışıyor | ### 5.4 Seeder tarafı (C#) — tercih edilen yol `api/src/Sozsoft.Platform.Domain.Shared/Editors/` ```csharp EditorScript = EditorScript.Build( EditorScript.Multiply("Quantity", "UnitPrice", "Total", digits: 2), EditorScript.Percent("Total", "VatRate", "VatAmount", 2, EditorScriptPercentMode.Add) .When(EditorScriptCondition.IsNotEmpty("VatRate")), EditorScript.ReadOnly("Total"), EditorScript.Today("OrderDate").OnOpen(), EditorScript.Copy("Customer.TaxNumber", "TaxNumber"), EditorScript.Ask("Fiyatı değiştirmek istediğinize emin misiniz?") .When(EditorScriptCondition.GreaterThan("UnitPrice", "1000"))); ``` Tek kurallı script için `EditorScriptRule` doğrudan `string`'e dönüşür: ```csharp EditorScript = EditorScript.Days("StartDate", "EndDate", "DayCount"); ``` > **Eşleme kuralı:** `scriptRecipes.ts` ile bu sınıflar aynı çıktıyı üretmek zorundadır. Birinde > bir tarif değişirse aynı görevde diğeri de değişir. --- ## 6. Artefakt C — `EditorOptions` DevExtreme editörüne geçirilen JSON. UI tarafındaki sözlük `optionSpecs.ts`, hazır kalıplar `presets.ts`, C# tarafı `EditorOptions` + `EditorOptionsBuilder`. | İhtiyaç | C# | Ürettiği JSON (özet) | | --- | --- | --- | | Pasif alan | `EditorOptions.Disabled()` | `{"disabled":true}` | | Salt okunur | `EditorOptions.ReadOnly()` | `{"readOnly":true}` | | Temizle düğmesi | `EditorOptions.ShowClearButton()` | `{"showClearButton":true}` | | Çok satırlı | `EditorOptions.Multiline(80)` | `{"height":80}` | | Sabit ondalık | `EditorOptions.Number(2)` | format + maske + spin | | Yüzde | `EditorOptions.Percent()` | | | Tarih / tarih-saat / saat | `EditorOptions.Date()` · `DateTime()` · `Time(15)` | | | Telefon | `EditorOptions.Phone()` | global format + maske + örnek | | Kaydırıcı | `EditorOptions.Slider()` | | | Görsel yükleme | `EditorOptions.ImageUpload(multiple: true, 80, 80)` | | | Zengin metin | `EditorOptions.Html(240)` | tam araç çubuğu | Akıcı ekleme: `.Placeholder()`, `.MaxLength()`, `.Height()`, `.HeightCss("100%")`, `.Width()`, `.Format()`, `.FixedPoint(p)`, `.DisplayFormat()`, `.SerializationFormat()`, `.Mask()`, `.Text()`, `.Number()`, `.Flag()`, `.Json()`. ```csharp EditorOptions = EditorOptions.Number(4).ShowClearButton(), EditorOptions = EditorOptions.Multiline(60).Placeholder("Açıklama"), EditorOptions = EditorOptions.New().Flag("acceptCustomValue", true), ``` Kurallar: - Aynı anahtar iki kez verilirse sonuncusu geçerlidir; anahtar sırası korunur. - `optionSpecs.ts` içinde `platform: true` işaretli ayarlar backend'de tipli DTO'ya (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`) çözülür; yanlış tip sessizce yok sayılır. - Preset'ler mevcut JSON ile **birleşir**, diğer ayarları silmez. --- ## 7. Artefakt D — Custom Component (Visual Designer) ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**, tasarımcıda üretilen bir custom component'tir. ### 7.1 Seed dosyası `configs/seeds/{kapsam}/custom/{Name}.json` ```jsonc { "GeneratedAt": "2026-08-20T11:23:45Z", "CustomComponents": [ { "Name": "OrderBoard", "RoutePath": "/admin/order-board", "Code": "/*__SOZSOFT_VISUAL_DESIGNER____*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard", "Props": "{\"visualDesigner\":{ … }}", "Description": "", "IsActive": true, "Dependencies": ["OrderCard"], "DataSources": [ { "Name": "Mrp_T_Order — GetList", "Method": "GET", "Url": "/api/app/crudendpoint/Mrp_T_Order", "ResponsePath": null, "EntityName": "Mrp_T_Order", "OperationType": "GetList", "SeedFile": "crud/Mrp_T_Order.json" } ] } ] } ``` - `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver` tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede kalır ama bu alanları boş gelir. - `Dependencies`, kanvasa bırakılan diğer custom component'lerin adlarıdır. ### 7.1.1 İki mod: hangisini yazacaksın? **Runtime `Code` alanını her zaman kullanmaz.** `getComponentRuntimeCode` şu kararı verir: ``` Props.visualDesigner varsa && version === 1 && sourceMode === 'visual' && nodes bir dizi ise → kod tasarımcı dokümanından YENİDEN ÜRETİLİR (generateDesignerCode), Code alanı yok sayılır aksi hâlde → Code alanı olduğu gibi Babel ile derlenir ``` Bu, prompt ile üretim için belirleyicidir: | Mod | Ne yazarsın | Ne zaman | | --- | --- | --- | | **Visual** (`sourceMode: "visual"`) | Yalnızca `Props.visualDesigner` dokümanı. `Code` alanı okunmaz; marker'lı bir yer tutucu bırakılır. | Ekran, tasarımcının bileşenleriyle ifade edilebiliyorsa. Kullanıcı sonradan kanvastan düzenleyebilir. **Varsayılan tercih.** | | **Code** (`Props: null`) | Doğrudan JSX. Tasarımcı kanvası olmaz. | Tasarımcı sözlüğünün karşılamadığı yerleşim/etkileşim gerekiyorsa. | Visual modda `Code` alanına şunu yaz — hem marker'ı taşır (tasarımcı dosyayı açabilsin) hem de derlense bile zararsızdır: ``` /*__SOZSOFT_VISUAL_DESIGNER____*/ const {Name} = () => { return ( <> ) } export default {Name} ``` Marker'ın içi `Props.visualDesigner` ile **aynı dokümanın** URL-encode edilmiş hâlidir. Tutarsız kalırsa tasarımcı `Props`'u esas alır; yine de ikisini eşit tut. ### 7.1.2 Code modunda çalışma ortamı Derlenen kod paylaşılan bir fonksiyon kapsamında çalışır. **`import` satırları temizlenir** — ihtiyacın olan her şey kapsamdan gelir: | Kapsamdaki ad | Ne | | --- | --- | | `React` | React'in tamamı (`React.useState`, `React.useEffect`…) | | `components/ui` dışa aktarımlarının **tamamı** | `Button`, `Input`, `Select`, `Dialog`, `Card`, `Notification`, `toast`, … (yeni bir UI bileşeni eklendiğinde otomatik gelir) | | `UiKit` | Aynı kitin isim uzayı hâli | | `PlatformViewHost` | Bir ListForm ekranını gömmek için | | `apiService` | Kimlik doğrulamalı HTTP istemcisi — **tercih edilen yol** | | `axios` | Ham HTTP (kimlik doğrulama eklemez) | | `DOMPurify` | HTML temizleme | | `translate(key)` | Dil anahtarı çözümü | | `checkPermission(name)` | Yetki kontrolü | | `getCulture()` | Aktif dil | Kurallar: - `export default {ComponentName}` zorunludur; ad dosya/kayıt adıyla birebir aynı olmalıdır. - Kullanıcıya görünen metni gömme; `translate('App.…')` kullan ve dil anahtarını wizard dosyasında ya da `LanguagesData.json`'da tanımla. - Veriye erişimde `apiService` kullan; taban adres, token ve tenant başlığı ondan gelir. Örneklerde gördüğün `axios.create({ baseURL: 'https://localhost:44344/' })` kalıbı **eski bir kayıttır, taklit etme**. - Yetkiyle gizlenecek her blok `checkPermission('App.…')` ile sarılır. Custom component route'u `authority: []` ile üretilir (§7.6 uyarısı) — kontrol bileşenin içindedir. - Başka bir custom component'i JSX olarak kullanabilirsin; adını `Dependencies` listesine ekle. ### 7.2 Tasarımcı dokümanı (`Props.visualDesigner`) ```ts DesignerDocument = { version: 1 sourceMode: 'visual' | 'code' // 'code' tek yönlüdür, kanvasa dönülmez nodes: DesignerNode[] canvas: { width: 'responsive' | 'desktop' | 'tablet' | 'mobile' } lifecycle: { onMount: string } // sayfa açılışında çalışan script dataSources: DesignerDataSource[] permissionCode?: string // Wizard'ın bu bileşen için ürettiği okuma yetkisi } DesignerNode = { id: string // 'cmp_…' — opak type: string // 'Form', 'Grid', 'Input', 'ListView', 'FlexRow' … kind: 'html' | 'ui' | 'layout' | 'platform' | 'custom' slot?: string ref?: string // 'btnSave' — script'ler birbirine bununla erişir props: Record events: Record // olay adı → script bindings: Record children: DesignerNode[] } DesignerBinding = { sourceId: string // 'source_…' (data source) ya da 'cmp_…' (kapsayıcı Form kaydı) path: string // okunacak kolon labelPath?: string valuePath?: string columns?: string[] // ekranda görünmeyen ek kolonlar; refs..getColumn('x') ile okunur } DesignerDataSource = { id: string name: string method: 'GET' | 'POST' | 'PUT' | 'DELETE' url: string responsePath: string // yanıt içindeki liste yolu; boşsa yanıtın kendisi filters?: DesignerDataSourceFilter[] } DesignerDataSourceFilter = { id: string field: string operator: 'eq'|'ne'|'contains'|'startswith'|'endswith'|'gt'|'gte'|'lt'|'lte'|'in'|'isnull'|'notnull' source: 'static' | 'query' | 'route' | 'record' value: string // static → değer · query/route → URL parametre adı · record → . previewValue?: string // yalnızca tasarımcı önizlemesi; üretilen koda girmez required?: boolean // true ise değer yoksa istek hiç atılmaz } ``` Filtre → query string sözleşmesi: `eq` çıplak parametredir (`?RoleId=5`), diğerleri adını son ek olarak taşır (`?Name.contains=abc`). Bu, CrudEndpoint `GetList`'in okuduğu sözleşmedir. > `required: true` varsayılandır ve bilinçlidir: değeri henüz gelmemiş bir filtre isteği > **tutmalıdır**; filtresiz koleksiyonu yüklemek "filtre çalışmıyor" gibi okunur. ### 7.3 Toolbox | Aile | Bileşenler | | --- | --- | | `layout` | `PageContainer` (maxWidth, padding, gap), `FlexRow` (columns, firstColumnWidth, gap, wrap, align), `Spacer` (height) | | `data` | `Form` — dört CRUD endpoint'ini sahiplenen kapsayıcı | | `platform` | `ListView`, `DataGridView`, `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — her biri `listFormCode` ile bir ListForm ekranını gömer | | `html` / `ui` | Ham HTML etiketleri ve `components/ui` bileşenleri (sözleşmeleri metadata'dan okunur) | | `custom` | Diğer custom component'ler | **Önce yeniden kullan:** istenen şey zaten bir ListForm ekranının verdiği liste/ağaç/grafikse, onu primitiflerden yeniden kurma — ilgili `platform` düğümünü `listFormCode` ile bırak. ### 7.3.1 Çalışan minimal doküman — bir Form + gömülü liste Aşağıdaki `Props` değeri (JSON string olarak yazılır) tek başına çalışır: üstte bir CRUD formu, altında var olan bir ListForm ekranı. ```jsonc { "visualDesigner": { "version": 1, "sourceMode": "visual", "canvas": { "width": "responsive" }, "lifecycle": { "onMount": "" }, "permissionCode": "App.Hr.AdvanceRequest", "dataSources": [ { "id": "source_list", "name": "Hr_T_AdvanceRequest — GetList", "method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" }, { "id": "source_byid", "name": "Hr_T_AdvanceRequest — GetById", "method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" }, { "id": "source_create", "name": "Hr_T_AdvanceRequest — Create", "method": "POST", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" }, { "id": "source_update", "name": "Hr_T_AdvanceRequest — Update", "method": "PUT", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" }, { "id": "source_delete", "name": "Hr_T_AdvanceRequest — Delete", "method": "DELETE", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" } ], "nodes": [ { "id": "cmp_page", "type": "PageContainer", "kind": "layout", "ref": "page", "props": { "maxWidth": "1280px", "padding": 24, "gap": 16 }, "events": {}, "bindings": {}, "children": [ { "id": "cmp_form", "type": "Form", "kind": "layout", "ref": "form1", "props": { "title": "::App.Hr.AdvanceRequest.Title", "selectEndpoint": "source_byid", "insertEndpoint": "source_create", "updateEndpoint": "source_update", "deleteEndpoint": "source_delete", "keyFieldName": "Id", "collectionPath": "", "keySource": "route", "keyParamName": "id", "autoLoad": true, "showToolbar": true, "columnCount": 2, "gap": 16, "selectPermission": "", "insertPermission": "", "updatePermission": "", "deletePermission": "" }, "events": {}, "bindings": {}, "children": [ { "id": "cmp_amount", "type": "Input", "kind": "ui", "ref": "inputAmount", "props": { "size": "md" }, "events": {}, "bindings": { "value": { "sourceId": "cmp_form", "path": "Amount" } }, "children": [] }, { "id": "cmp_note", "type": "Input", "kind": "ui", "ref": "inputNote", "props": { "textArea": true, "rows": 3 }, "events": {}, "bindings": { "value": { "sourceId": "cmp_form", "path": "Description" } }, "children": [] } ] }, { "id": "cmp_list", "type": "ListView", "kind": "platform", "ref": "listView1", "props": { "listFormCode": "App.Hr.AdvanceRequests", "height": "520px", "className": "mt-4", "designerPermission": "App.Hr.AdvanceRequests", "filters": [ { "id": "filter_1", "field": "EmployeeId", "operator": "eq", "source": "record", "value": "form1.EmployeeId", "previewValue": "", "required": true } ] }, "events": {}, "bindings": {}, "children": [] } ] } ] } } ``` Doküman yazarken uyulacak kurallar: - **`id` benzersiz ve kalıcıdır.** Bağlamalar (`bindings.sourceId`) bu id'lere işaret eder; değiştirirsen bağ kopar. `cmp_…` / `source_…` öneki bir sözleşmedir, zorunlu değildir ama koru. - **`ref` script'lerin adres defteridir**; olay script'leri birbirine `refs.inputAmount…` ile erişir. Okunur ve tekil tut. - Form içindeki bir alanın kaynağı **Form düğümünün `id`'sidir** (`sourceId: "cmp_form"`), data source değil. Data source'a bağlanan şeyler listeler/seçim bileşenleridir. - Her düğümde `props`, `events`, `bindings`, `children` **var olmalıdır** (boş olsalar bile). - `kind` değeri toolbox ailesiyle uyumlu olmalıdır; yanlış `kind` düğümü çizdirmez. - **`::` öneki dil anahtarıdır.** Metin taşıyan bir prop `::` ile başlıyorsa üretilen kodda `translate('::…')` çağrısına dönüşür; başlamıyorsa düz metin olarak gömülür. Kullanıcıya görünen her metni `::App.…` biçiminde yaz (`"title": "::App.Hr.AdvanceRequest.Title"`). - **`platform` düğümleri `filters` prop'u alır.** Gömülü ListForm ekranını ana kayda ya da URL'ye bağlamak için `DesignerDataSourceFilter` ile aynı şekli kullanır; `source: "record"` iken `value` `"{formRef}.{Kolon}"` biçimindedir (`"form1.EmployeeId"`). `required: true` ise ana kayıt gelmeden liste yüklenmez. - Anahtar kolon adında **büyük/küçük harf endpoint'in döndürdüğüyle aynı olmalıdır** (`keyFieldName: "id"` ile `"Id"` farklı şeylerdir); CRUD endpoint'in yanıtına bak. ### 7.4 `Form` bileşeni | Prop | Anlam | | --- | --- | | `selectEndpoint` / `insertEndpoint` / `updateEndpoint` / `deleteEndpoint` | **Data source `id`'si** (`"source_5w_5ibpv"`) — URL değil. Endpoint'in kendisi `dataSources` listesinde tanımlıdır. | | `keyFieldName` | Anahtar kolon | | `collectionPath` | Select yanıtında liste yolu | | `keySource` + `keyParamName` | Anahtarın nereden geldiği (route/query/sabit) | | `previewKeyValue` | Yalnızca tasarımcı önizlemesi | | `autoLoad` | Açılışta Select çalışsın mı | | `showToolbar` | Kaydet/Sil araç çubuğu | | `columnCount` / `gap` | İç yerleşim | | `title` | Dolu ise Card başlığı; boş ise başlık düşer | İçine bırakılan her bileşen Select sonucunun bir kolonuna bağlanır (`bindings.value.sourceId` = Form düğümünün `id`'si) ve Save/Delete üzerinden geri yazar. Olaylar: `onLoad`, `onRecordChange`, `onFieldChange`, `onNewRecord`, `onModeChange`, `onBeforeSave`, `onAfterSave`, `onBeforeDelete`, `onAfterDelete`, `onError`. ### 7.5 Yetki modeli (zorunlu) İki katman: 1. **Düğüm görünürlüğü** — `props.designerPermission` dolu olan düğüm, yetki yoksa hiç render edilmez. Veriyi göremeyecek kullanıcıya boş bir Grid göstermek yerine bileşen çizilmez. 2. **Form komutları** — `selectPermission` / `insertPermission` / `updatePermission` / `deletePermission`. - `Otomatik` mod: temel, dokümandaki `permissionCode`'dur (Wizard'ın bu bileşen için ürettiği okuma yetkisi, menü kaydının korunduğu yetkinin aynısı). Komutlar sırasıyla `''`, `.Create`, `.Update`, `.Delete` son eklerini alır. - `Özel` mod: yetki adı elle yazılır. - Boş bırakılan komut serbesttir. Manager ekranı yetkileri: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. ### 7.6 Bileşeni menüye bağlama Bileşen tek başına bir ekran değildir; menü ve yetki Wizard'ın `Custom` yolundan gelir: ```jsonc { "Wizard": { "ComponentKind": 1, "WizardName": "OrderBoard", "ListFormCode": "App.Mrp.OrderBoard", "MenuCode": "App.Mrp.OrderBoard", "CustomComponentName": "OrderBoard", "MenuUrl": "/admin/order-board", "CreateMenu": true, "MenuParentCode": "App.Mrp", "PermissionGroupName": "App.Mrp", "PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP", "LanguageTextMenuEn": "Order Board", "LanguageTextMenuTr": "Sipariş Panosu", "MenuIcon": "FcKanban", "MenuOrder": 2 }, "IsDeletedField": false, "IsCreatedField": false, "InsertedRecords": { "LanguageKeys": ["App.Mrp.OrderBoard"], "PermissionGroupNames": [], "PermissionNames": ["App.Mrp.OrderBoard", "App.Mrp.OrderBoard.Create", "App.Mrp.OrderBoard.Update", "App.Mrp.OrderBoard.Delete"], "MenuCodes": ["App.Mrp.OrderBoard"], "DataSourceCodes": [] } } ``` Custom yolunda `ListForm` **üretilmez**; `Title`/`Desc` dil anahtarları da üretilmez. `Export`/`Import`/`Note` yetkileri bu yolda yoktur. > **Güvenlik — route yetkisiz üretilir.** Custom component route'ları `RoutePath` değerinden > türetilir ve `authority: []` ile kaydedilir; `/admin/` ile başlayan yol `protected`, diğerleri > `public` olur. Yani **URL'yi bilen her oturum açmış kullanıcı sayfayı açabilir**. Menü yetkisi > yalnızca menüde görünmeyi engeller. Gerçek koruma bileşenin içindedir: > `designerPermission` ile düğümleri gizle, Form komutlarını `*Permission` ile kapat, kod modunda > `checkPermission()` ile sar. Yetkisiz kullanıcıya boş bir sayfa gösteren bir bileşen doğru > sonuçtur; veri gösteren bileşen açıktır. > > `RoutePath` `/admin/` ile başlamıyorsa sayfa **herkese açıktır** — bilinçli değilse başlat. ### 7.6.1 Depodaki gerçek örnek — `RoleComponent` + `RoleList` wizard Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar: ``` configs/seeds/host/custom/RoleComponent.json configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json configs/seeds/host/wizard/20260819131500_RoleList.json ``` Nasıl bağlanıyorlar: | Bağ | Değer | | --- | --- | | Wizard `ComponentKind` | `1` (Custom) | | Wizard `CustomComponentName` | `RoleComponent` | | Component `RoutePath` | `/admin/roles` | | Wizard `MenuUrl` | `/admin/roles` — component'in `RoutePath` değeriyle **aynı** | | Wizard `MenuCode` / `ListFormCode` | `App.Wizard.RoleList` | | Component `Props.visualDesigner.permissionCode` | `App.Wizard.RoleList` — wizard'ın ürettiği okuma yetkisi | | Wizard `InsertedRecords.PermissionNames` | `App.Wizard.RoleList` + `.Create` + `.Update` + `.Delete` | | Wizard `InsertedRecords.LanguageKeys` | `App.Wizard.RoleList` | | Component `DataSources[].SeedFile` | `crud/AbpRoles.json` vb. | **Okunacak dersler:** 1. `permissionCode` **wizard'ın ürettiği yetkinin aynısıdır**. Form komutları `Otomatik` modda bu koda `.Create`/`.Update`/`.Delete` ekleyerek çözülür. Component'i yazarken bu değeri wizard'daki `MenuCode` ile aynı yap. 2. `RoutePath` ile wizard'ın `MenuUrl` değeri **birebir aynı** olmalıdır; farklıysa menü başka bir yere gider. 3. Component'in `DataSources` listesi `crud/*.json` dosyalarına `SeedFile` ile işaret eder — export zip'inin bu dosyaları da toplamasını sağlayan şey budur. 4. Wizard `Custom` yolunda olsa bile `ListFormCode`, `Grid/Card/Pivot/Chart` bayrakları gibi List alanlarını taşır; **bunlar kullanılmaz**, varsayılan değerleriyle dosyada kalır. Yeni dosya yazarken bunları temizlemeye çalışma, şablonu koru. 5. `platform` düğümündeki `listFormCode` (`AbpIdentity.Roles`) **var olan bir ListForm kodudur**; component ekranın kendisini yeniden kurmaz, gömer. ### 7.7 Taşıma Bir ekranı başka ortama taşımak tek dosya kopyalamak değildir. Wizard File Manager'ın **Export** butonu zip üretir: ``` wizard/{dosya}.json custom/{component}.json (bağımlılıklarıyla) crud/{entity}.json (component'lerin kullandığı endpoint'ler) {sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi) ``` Import iki adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür) → uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma). Sınırlar: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi. Yetkiler: `App.Listforms.Wizard.Export` / `.Import`. --- ## 8. Artefakt E — SQL nesneleri ### 8.1 Tablo tasarımcısı — varsayılan kolonlar Kullanıcı yalnızca iş kolonlarını söylese bile şunlar varsayılan olarak eklenir: `Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcı açıkça "tenant yok" / "audit yok" demedikçe çıkarma; her seferinde sorma. Deploy öncesi adımda CRUD endpoint üretimi ve hangi operasyonların aktif başlayacağı seçilir; seçilmeyenler pasif kaydedilir, sonradan diyalogdan açılabilir. ### 8.2 View tasarımcısı Bir ekran birden fazla tablodan besleniyorsa, `SelectCommandType: 4` (Query) yerine **tasarlanmış bir view** (`SelectCommandType: 2`) tercih edilir: yeniden kullanılabilir, seed'lenir ve tasarımcıda görsel olarak düzenlenebilir kalır. Model: kaynaklar (tablo/view, `CROSS`/`OUTER APPLY`, türetilmiş alt sorgu), JOIN'ler (`INNER`/`LEFT`/`RIGHT`/`FULL`/`CROSS`; `=`, `<>`, `>`, `>=`, `<`, `<=`) ve criteria satırları (Column / Alias / Output / Group By / Sort / Filter / Or…). Group By sütunu: `GroupBy`, `Where` (satır çıktıya girmez, yalnızca filtre taşır), `SUM`, `COUNT`, `COUNT_DISTINCT`, `AVG`, `MIN`, `MAX`. Filtre hücreleri serbest yüklemdir (`> 100`, `LIKE '%abc%'`, `IS NULL`). **Tek yönlüdür.** Model → T-SQL. Var olan bir view kanonik şekle uymuyorsa geri okunamaz ve dialog ham SQL moduna düşer. Bu yüzden tasarlanmış bir view'ı elle düzenlemek tasarımcıyı kaybettirir. Nesneler `configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına yazılır: `object` = yeniden oluşturulabilir nesne (tablo/view/fonksiyon/prosedür), `execute` = bir kez çalıştırılacak script. ### 8.3 SQL seed dosyası yazım kuralları | Kural | Açıklama | | --- | --- | | Dosya adı | Nesne adıyla **birebir aynı**: `Hr_T_AdvanceRequest.sql` | | Klasör | `object` = idempotent nesne tanımı · `execute` = nesne olarak da oluşturulur, ayrıca tüm migration'lar bittikten sonra çalıştırılır | | Sağlayıcı | Hedef SQL Server ise `sql/`, PostgreSQL ise `postgres/`. **İkisini birden destekliyorsan iki dosya da yazılır**; içerikler aynı nesnenin iki diyalektidir. | | İdempotency | SQL Server: `CREATE OR ALTER VIEW/PROCEDURE/FUNCTION`, tablolar için `IF NOT EXISTS` koruması. PostgreSQL: `CREATE OR REPLACE VIEW/FUNCTION`, `CREATE TABLE IF NOT EXISTS`. | | Şema | `dbo` kullanılır; PostgreSQL tarafında tanımlayıcılar çift tırnaklıdır (`dbo."Hr_T_AdvanceRequest"`) çünkü kolon adları PascalCase'tir. | | Toplu ayraç | SQL Server'da prosedür sonunda `GO` kullanılabilir; PostgreSQL'de kullanılmaz. | | Yorum | Dosyanın ilk satırında ne ürettiğini yaz (`-- Create View`). | Tablo yazarken §8.1'deki varsayılan kolonlar dahil edilir. Örnek iskelet (SQL Server): ```sql -- Create Table IF OBJECT_ID(N'[dbo].[Hr_T_AdvanceRequest]', N'U') IS NULL BEGIN CREATE TABLE [dbo].[Hr_T_AdvanceRequest] ( [Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Hr_T_AdvanceRequest] PRIMARY KEY, [TenantId] UNIQUEIDENTIFIER NULL, -- iş kolonları buraya [CreationTime] DATETIME2 NOT NULL, [CreatorId] UNIQUEIDENTIFIER NULL, [LastModificationTime] DATETIME2 NULL, [LastModifierId] UNIQUEIDENTIFIER NULL, [IsDeleted] BIT NOT NULL CONSTRAINT [DF_Hr_T_AdvanceRequest_IsDeleted] DEFAULT (0), [DeletionTime] DATETIME2 NULL, [DeleterId] UNIQUEIDENTIFIER NULL ); END ``` > `Id` tipini ne seçtiysen wizard dosyasındaki `KeyFieldDbSourceType` ile aynı olmalıdır > (`UNIQUEIDENTIFIER` → `9`, `INT IDENTITY` → `11`). Uyumsuzluk sessiz veri hatası üretir. ### 8.4 Seeder bu dosyaları nasıl çalıştırır Yeni bir entity/tablo `sql/object/` altına yazılır ve veritabanı `SqlDataSeeder` tarafından bu dosyalardan kurulur. Mekanik: 1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise `{kapsam}/sql/`, PostgreSQL ise `{kapsam}/postgres/`. İki dosya da yazmak taşınabilirlik içindir; aynı anda ikisi birden çalışmaz. 2. Sıra: `object/` → `execute/`, her klasörün içinde **dosya adına göre alfabetik**. Bir tablo başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql` `Hr_T_EmployeeAdvance.sql`'den önce gelir). 3. Dosya içeriği `GO` satırlarından batch'lere bölünür ve sırayla çalıştırılır. PostgreSQL'de `GO` kullanılmaz. 4. **Hata seed'i durdurur.** Bir batch patlarsa istisna yukarı fırlatılır ve o kapsamın seed'i yarım kalır. Bu yüzden her script tekrar tekrar çalıştırılabilir (idempotent) olmalıdır: `CREATE OR ALTER` / `CREATE OR REPLACE` / `IF NOT EXISTS`. 5. SQL nesneleri bittikten hemen sonra **aynı kapsam için `CrudDataSeeder`** çalışır; böylece CRUD endpoint'leri dayandıkları tablolardan sonra oluşur. Tablo dosyasını yazmadan `crud/{Entity}.json` yazmak anlamsızdır. 6. Klasör kökünde kalmış `.sql` dosyaları geriye dönük uyumluluk için çalıştırılır ama **uyarı loglar**; yeni dosyayı her zaman `object/` ya da `execute/` altına koy. **`execute/` klasörünün özel sözleşmesi:** buradaki dosyalar iki aşamalıdır. - 1. aşama: `SqlDataSeeder` dosyayı çalıştırır → nesne oluşur/güncellenir. - 2. aşama: tüm migration ve seed işlemleri bittikten sonra `AfterAllMigrationsSqlExecutor` **dosya adından türettiği stored procedure'ü çağırır**. Yani `execute/` altındaki bir dosya, **adı prosedür adıyla aynı olan bir stored procedure tanımlamalıdır** (`Hr_T_AdvanceRequest_Backfill.sql` → `Hr_T_AdvanceRequest_Backfill` prosedürü). `schema.Procedure.sql` biçimi de desteklenir. Prosedür adı güvenli tanımlayıcı deseninden geçer (harf/alt çizgi ile başlar, en fazla 127 karakter). Sadece bir DDL çalıştırmak istiyorsan dosya `object/` altına aittir; `execute/` veri dolumu ve migration sonrası düzeltme içindir. ### 8.5 Yeni entity/tablo eklerken izlenecek yol 1. `sql/object/{Modul}_T_{Entity}.sql` — tabloyu §8.3 iskeletiyle yaz (tenant + audit kolonları varsayılan). PostgreSQL hedefi de destekleniyorsa `postgres/object/` altına diyalekt eşini yaz. 2. İndeks ve FK'leri **aynı dosyada**, varlık kontrolüyle ekle (`IF NOT EXISTS`); ayrı dosya sıralamayı kırılgan hâle getirir. 3. Ekran bu tabloyu doğrudan kullanacaksa CRUD endpoint gerekmez — ListForm `SelectCommandType: 1` ile tabloya bağlanır. Custom Component kullanacaksa `crud/{Entity}.json` de yaz (§9). 4. Wizard dosyasında `SelectCommand` = tablo adı, `KeyFieldName` = `Id`, `KeyFieldDbSourceType` = tablodaki `Id` tipinin sayısı, `IsTenant` = tabloda `TenantId` varsa `true`, `IsDeletedField`/`IsCreatedField` = ilgili kolonlar varsa `true`. 5. DB Migrate çalıştır; log'da `Executing: CREATE TABLE …` satırını gör. --- ## 9. Artefakt F — CRUD Endpoint Konum: `configs/seeds/{kapsam}/crud/{EntityName}.json` · Okuyan: `CrudDataSeeder` ```jsonc { "EntityName": "Mrp_T_Order", "GeneratedAt": "2026-08-20T06:13:28Z", "Endpoints": [ { "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "GetList", "IsActive": true }, { "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "GetById", "IsActive": true }, { "Method": "POST", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "Create", "IsActive": true }, { "Method": "PUT", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Update", "IsActive": true }, { "Method": "DELETE", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Delete", "IsActive": true } ] } ``` Üretilen C# kodu dosyada tutulmaz; entity adı ve operasyondan yeniden türetilir. - Üretim/aktivasyon yeri: SQL Query Manager nesne gezgininde tablo satırının **CRUD Endpoints** aksiyonu, toolbar'daki toplu üretim, tablo tasarımcısının deploy öncesi adımı ve Wizard'ın veri ayarları adımı — hepsi aynı `CrudEndpointDialog`. - Yetki: `App.SqlQueryManager.CrudEndpoints`. İzin yoksa butonlar görünmez ve `crud-endpoint-generate` uçlarının tamamı reddedilir. - Dispatcher üzerinden çağrı kapıları: `App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir. - `GetList` filtre sözleşmesi §7.2'deki ile aynıdır. --- ### 9.2 Custom Endpoint — elle yazılmış SQL'i API olarak açmak Ekran: `/admin/list/App.DeveloperKit.CustomEndpoints` (bir ListForm ekranıdır). Entity: `CustomEndpoint`. | Alan | Anlam | | --- | --- | | `Name` / `Description` | Tanımlayıcı | | `Url` | Yayınlanacak yol | | `Method` | `GET` / `POST` / `PUT` / `DELETE` | | `DataSourceCode` | Hangi bağlantıdan okunacağı. **`"!Tenant"`** özel değeridir: isteği yapan kullanıcının kendi tenant veritabanından okunur. | | `Sql` | Parametreli sorgu. String birleştirme yasak. | | `ParametersJson` | `CustomEndpointParameter[]` | | `PermissionsJson` | `CustomEndpointPermission[]` | ```jsonc // ParametersJson [ { "Type": "Query", "Name": "customerId", "DefaultValue": "", "Path": "", "IsRequired": true }, { "Type": "Path", "Name": "id", "DefaultValue": "", "Path": "", "IsRequired": true }, { "Type": "Body", "Name": "note", "DefaultValue": "", "Path": "data.note", "IsRequired": false }, { "Type": "Static","Name": "tenantMode", "DefaultValue": "1", "Path": "", "IsRequired": true } ] // PermissionsJson — Global / Role / User [ { "ResourceType": "Role", "ResourceId": "SalesManager" }, { "ResourceType": "User", "ResourceId": "" } ] ``` Parametre tipleri: `Static` (sabit), `Query` (query string), `Path` (URL segmenti), `Body` (gövde; `Path` ile gövde içindeki yol). Yetki iki katmanlıdır: önce dispatcher kapısı (`App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`), sonra endpoint'in kendi `PermissionsJson` kuralları. Endpoint tanımlarını düzenleme yetkileri ise ListForm ekranının `App.DeveloperKit.CustomEndpoints{,.Create,.Update,.Delete}` yetkileridir. **Ne zaman CRUD, ne zaman Custom Endpoint?** Tek tabloya standart CRUD gerekiyorsa CRUD Endpoint (otomatik üretilir, seed'lenir, filtre sözleşmesi hazırdır). Birleştirilmiş/şekillendirilmiş sorgu, rapor ya da özel bir yol gerekiyorsa Custom Endpoint. ### 9.3 Dynamic Service — çalışma zamanında derlenen C# servisi Ekran: `/admin/developerkit/dynamic-services`. Entity: `DynamicService`. Bu, karar sırasının **3. adımıdır**; konfigürasyon ve SQL yetmediğinde gelir, kod değişikliğinden önce denenir. | Alan | Anlam | | --- | --- | | `Name` | Benzersiz servis adı (`DynamicCustomerService`) | | `DisplayName` / `Description` | Kullanıcı dostu başlık ve açıklama | | `Code` | Tam C# sınıfı — `using`'ler ve `namespace` dahil | | `ControllerName` | Swagger'da görünecek controller adı | | `PrimaryEntityType` | Kullandığı ana entity (opsiyonel) | | `IsActive` | Yayında mı | | `CompilationStatus` | `Pending(0)` · `Success(1)` · `Failed(2)` · `InProgress(3)` | | `LastCompilationError` / `LastSuccessfulCompilation` | Son derleme sonucu | | `Version` / `CodeHash` | Kod her değiştiğinde `Version` artar, hash yenilenir | Yaşam döngüsü: **TestCompile** (Roslyn ile derle, hata mesajı UI'a döner) → **Publish** → `DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder → `ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir → `DynamicControllerActivator` bağımlılıkları enjekte eder. **Uygulama yeniden başlatılmaz.** Yetkiler: `App.DeveloperKit.DynamicServices{.Create,.Edit,.Delete,.Manage,.TestCompile,.Publish,.ViewCode}`. Yazarken `dotnet.instructions.md` §5 geçerlidir: `PlatformAppService` türet, `[Authorize(...)]` ile koru, tenant bağlamını `ICurrentTenant` üzerinden kullan, SQL'i parametreli çalıştır, kullanıcıya görünen metni `PlatformResource` anahtarından ver, sabit/secret gömme. --- ### 9.4 Developer Kit menüsü — tam kapsam Menü: `App.DeveloperKit` (üst) → aşağıdaki alt ekranlar. Her satır bu dokümandaki bölümüne bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir. | Ekran | Route | Yetki | Ne üretir | Bölüm | | --- | --- | --- | --- | --- | | **SQL Query Manager** | `/admin/sqlQueryManager` | `App.SqlQueryManager` | SQL nesneleri, CRUD endpoint'ler | 9.5 | | **Custom Endpoints** | `/admin/list/App.DeveloperKit.CustomEndpoints` | `App.DeveloperKit.CustomEndpoints` | Elle yazılmış SQL'den REST endpoint | 9.2 | | **Dynamic Services** | `/admin/developerkit/dynamic-services` | `App.DeveloperKit.DynamicServices` | Runtime derlenen C# AppService | 9.3 | | **Components** | `/admin/developerkit/components` | `App.DeveloperKit.Components` | Custom Component + Visual Designer | 7 | | **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 | | **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi, export/import | 4, 7.7 | ### 9.5 SQL Query Manager — bileşen bileşen Tek ekranda toplanmış araç seti (`SqlQueryManager.tsx`): | Panel / araç | Dosya | Yapabildikleri | | --- | --- | --- | | **Veri kaynağı seçici** | — | `DataSource` kayıtları arasında geçiş; SQL Server ve PostgreSQL | | **Nesne gezgini** | `SqlObjectExplorer.tsx` | Tablolar, view'lar, stored procedure'ler, fonksiyonlar ve hazır şablonlar (`SqlObjectExplorerDto`); arama, nesne tanımını açma, kopyalama, silme, satır bazında **CRUD Endpoints** aksiyonu, çoklu seçimle toplu endpoint üretimi | | **Sorgu editörü** | `SqlEditor.tsx` | Monaco tabanlı; çalıştırma (`ExecuteQueryAsync`), çoklu sekme, nesne tanımını editöre yükleme (`GetNativeObjectDefinitionAsync`), tablo create script'i üretme (`GetTableCreateScriptAsync`) | | **Sonuç grid'i** | `SqlResultsGrid.tsx` | Sonuç kümesi görüntüleme ve dışa aktarma | | **Tablo tasarımcısı** | `SqlTableDesignerDialog.tsx` | Kolon/anahtar/index tasarımı, varsayılan tenant + audit kolonları, deploy öncesi CRUD endpoint seçimi | | **View tasarımcısı** | `SqlViewDesignerDialog.tsx` + `sqlViewDesigner/` | Diagram + criteria grid + T-SQL önizleme; bkz. 8.2 | | **CRUD endpoint diyaloğu** | `CrudEndpointDialog.tsx` | Endpoint üretme, aktif/pasif etme, test, silme; aynı diyalog Wizard'ın veri adımında da açılır | 9 | | **DB Migrate** | `DbMigrateButton` | Migration + seed tetikleme (`App.Setup.Migrate`) | | **File Manager kısayolu** | `/admin/files` | `configs/seeds` altındaki seed dosyalarının yönetimi | | **Kolon bilgisi** | `GetTableColumnsAsync` | Wizard alan adımının ve tasarımcıların kolon kaynağı | Servis yüzeyi (`SqlObjectManagerAppService`): `GetAllObjectsAsync`, `ExecuteQueryAsync`, `GetNativeObjectDefinitionAsync`, `GetTableColumnsAsync`, `GetTableCreateScriptAsync`. Kalıcılık kuralı: tasarımcıdan çıkan her nesne `configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına düşer. `object` yeniden oluşturulabilir nesneler (tablo/view/fonksiyon/prosedür), `execute` bir kez çalışacak script'ler içindir. Editörde elle çalıştırılan ama dosyaya düşmeyen bir DDL, veritabanı yeniden kurulduğunda kaybolur. ### 9.6 ListForm ekranı ve editörü Wizard bir ekranı **üretir**; ince ayar `/admin/listform/edit/{listFormCode}` üzerinden yapılır. Sekmeler ve karşılık geldikleri JSON kolonları: | Sekme | JSON | | --- | --- | | Veritabanı | `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `Insert/Update/DeleteCommand` (+ `Before`/`After`), `*ServiceAddress` | | Sütunlar | `ListFormField` kayıtları, `ColumnOptionJson`, banded/command sütunlar | | Filtreler | `FilterRowJson`, `HeaderFilterJson`, `FilterPanelJson`, `SearchPanelJson`, `GroupPanelJson`, `ExtraFilterJson` | | Düzenleme | `EditingOptionJson`, `EditingFormJson` | | Yetkiler | `PermissionJson` (ekran ve sütun düzeyi) | | Alt Form | `SubFormsJson` | | Pivot / Tree / Gantt / Scheduler / Todo | `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` | | Widget | `WidgetsJson` | | Workflow | `WorkflowJson` | | Durum | `StateStoringJson`, `PagerOptionJson`, `SelectionJson` | | Grafik | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `ZoomAndPanJson` | Ayrıca `CustomJsSourcesJson` / `CustomStyleSourcesJson` ekran yüklendiğinde çalışacak JS/CSS taşır — son çaredir, önce `EditorScript`/`EditorOptions` denenmelidir. > **Uyarı:** Bu ekrandan yapılan değişiklikler doğrudan veritabanına yazılır ve wizard seed > dosyasına **yansımaz**. Kalıcı olması gereken bir değişikliği ya wizard'ı `EditFileName` ile > yeniden çalıştırarak ya da seed dosyasını güncelleyerek yap. --- ### 9.7 Menü Yönetimi ekranları Menü: `App.Menus` (üst) → dört ekran. **Wizard bunların çoğunu senin yerine yapar**; bu ekranlar elle düzeltme ve wizard'ın kapsamadığı düzenlemeler içindir. | Ekran | Route | Yetki | Ne yapar | | --- | --- | --- | --- | | **Routes** | `/admin/list/App.Menus.Routes` | `App.Menus.Routes` | Fiziksel React sayfalarının route kayıtları (`Key`, `Path`, `ComponentType`, `ComponentPath`, `RouteType`, `Authority[]`) | | **Menu Groups** | `/admin/list/App.Menus.MenuGroup` | `App.Menus.MenuGroup` | Menü grubu adları (`Erp`, `Kurs` gibi üst kümeler) | | **Menu List** | `/admin/list/App.Menus.Menu` | `App.Menus.Menu` | Menü kayıtlarının ham listesi | | **Menu Manager** | `/admin/menuManager` | `App.Menus.Manager` | Sürükle-bırak menü ağacı; sıra ve hiyerarşi düzenleme | **`Menu` kaydının alanları** (wizard bunları üretir; elle eklemen gerekirse): | Alan | Anlam | | --- | --- | | `Code` | Benzersiz kod; yetki adının kökü | | `DisplayName` | **Dil anahtarı** (düz metin değil) | | `ParentCode` | Üst menü; boş ise kök menü | | `Url` | Açılacak adres | | `Icon` | react-icons adı | | `Order` | Kardeşler arası sıra | | `ShortName` | Kısa rozet (`HR`, `MRP`) — kök menülerde kullanılır | | `RequiredPermissionName` | Görünürlük yetkisi | | `Target` / `CssClass` / `ElementId` / `IsDisabled` | Bağlantı davranışı ve görünüm | | `UserId` / `RoleId` / `CultureName` | Kullanıcı, rol ve dil bazlı menü varyantı | **Kurallar — prompt ile üretim yaparken:** 1. **Menüyü elle ekleme.** Yeni bir ekranın menüsü wizard seed dosyasından gelir; `MenusData.json` yalnızca platformun kendi ekranları içindir ve orada da `Routes` bölümüne dokunulmaz. 2. **Route kaydı üretme.** Dinamik liste ekranları statik `/admin/list/:listFormCode` route'unu kullanır; custom component'lerin route'u `RoutePath` alanından türetilir. `Route` tablosu yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa yazılmaz. 3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan; Menu Manager'da elle taşımak seed dosyasına yansımaz ve veritabanı sıfırlandığında kaybolur. 4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır — aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez. --- ### 9.8 Wizard Manager — layout matrisi Wizard Manager platformun en yetenekli üretim ekranıdır: tek bir tanımdan **hem Custom Component sayfası hem de yedi farklı görünümü olan List ekranı** üretir. Bir `List` wizard'ında birden fazla görünüm aynı anda açık olabilir; kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı saklanır. | Görünüm | Bayrak | `DefaultLayout` | Zorunlu option alanı | Ne zaman aç | | --- | --- | --- | --- | --- | | Grid | `Grid` | `"grid"` | — | Neredeyse her zaman; varsayılan | | Card | `Card` | `"card"` | — | Görsel/özet kayıtlar, mobil kullanım | | Pivot | `Pivot` | `"pivot"` | — | Sayısal kırılım/analiz ihtiyacı | | Chart | `Chart` | `"chart"` | — (grafik ayarları ListForm editöründe) | Trend/dağılım | | Tree | `Tree` | `"tree"` | `TreeOptionDto.KeyExpr` + `ParentIdExpr` | Kendine referanslı hiyerarşi | | Gantt | `Gantt` | `"gantt"` | `GanttOptionDto.ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` | Zaman çizelgesi, bağımlılıklı görevler | | Scheduler | `Scheduler` | `"scheduler"` | `SchedulerOptionDto.TextExpr` + `StartDateExpr` + `EndDateExpr` | Takvim/randevu | | TodoBoard (Kanban) | `Todo` | `"todo"` | `TodoOptionDto.TitleExpr` + `StatusExpr` | Durum akışı olan işler | Kurallar: - Zorunlu option alanı boşsa seeder o görünümün JSON'unu **yazmaz**; bayrak açık kalsa bile görünüm çalışmaz. Açtığın her görünümün option bloğunu doldur. - `DefaultLayout`, bayrağı `true` olan bir görünüm olmalıdır. - Aynı veri birden fazla görünümde aynı `SelectCommand` üzerinden sunulur; ayrı sorgu yazılmaz. - Bir ekranın hem liste hem pano hem takvim olarak istenmesi **tek wizard dosyasıdır** — ikinci bir ekran açma. Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenleme (`EditFileName`), silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. --- ## 10. Uçtan uca reçeteler ### 10.1 "Var olan bir tablo için ekran aç" 1. Tablo yoksa SQL Table Designer ile oluştur (varsayılan kolonlarla). 2. Ekran birden çok tablodan besleniyorsa View Designer ile view üret. 3. Wizard: menü/kimlik → veri ayarları (`SelectCommandType` + `SelectCommand` + anahtar) → alanlar → (gerekiyorsa) alt form/widget/workflow → yayınla. 4. Hesaplanan alanlara `EditorScript`, biçimli alanlara `EditorOptions` ver. 5. Doğrula: menü görünüyor mu, yetkiler roldeki kullanıcı için doğru mu, tenant filtresi çalışıyor mu, seed dosyası oluştu mu. ### 10.2 "Ana-detay ekranı" 1. Detay ekranını **menüsüz wizard** ile üret (`CreateMenu: false`). 2. Ana ekranın `SubForms` listesine `ParentFieldName` → `ChildFieldName` eşlemesini ve `DbType` ekle. 3. İki ekranın yetkileri ayrıdır; detayın yetkisi verilmemişse sekme görünmez. ### 10.3 "Serbest yerleşimli sayfa / dashboard" 1. Gerekli endpoint'ler yoksa CRUD Endpoint üret. 2. Component Manager → yeni bileşen → Visual Designer: `PageContainer` → `FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`. 3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla kur, `required` bayrağını bilinçli seç. 4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`). 5. Wizard `ComponentKind: 1` ile menüye bağla. 6. Doğrula: `custom/{Name}.json` ve ilgili `crud/*.json` dosyaları oluştu mu. ### 10.4 "Kanban panosu" 1. Tabloda durum kolonu olsun (`Status`). 2. Wizard'da `Todo: true`, `DefaultLayout: "todo"`, `TodoOptionDto` içinde `TitleExpr`, `StatusExpr`, `StatusOrder` (kolon sırası), `AssigneeExpr`, `DueDateExpr` doldur. 3. Kolonlar `statusExpr` değerlerinden üretilir; kartlar sürüklenerek durum değiştirir, panodan yeni kolon eklenebilir. --- ### 10.5 "Rapor sorgusunu API olarak aç" 1. Sorguyu SQL Query Manager'da yaz ve çalıştır, sonucu doğrula. 2. Tekrar kullanılacaksa view olarak deploy et (`sql/object/…`), tek seferlikse Custom Endpoint içinde bırak. 3. Custom Endpoints ekranından kaydı oluştur: `Url`, `Method`, `DataSourceCode` (kullanıcının kendi tenant veritabanı isteniyorsa `"!Tenant"`), `Sql`, `ParametersJson`, `PermissionsJson`. 4. Parametreleri `Query`/`Path`/`Body` olarak tanımla; hiçbirini SQL'e string birleştirme ile koyma. 5. Erişimi `PermissionsJson` ile daralt; dispatcher kapısının (`…CustomEndpoints.Get` vb.) ilgili rolde açık olduğunu doğrula. ### 10.6 "Konfigürasyonla çözülemeyen iş mantığı" 1. Önce gerçekten çözülemediğini göster: ListForm + SQL + Custom Endpoint neden yetmiyor, bir iki cümleyle yaz. 2. Dynamic Service oluştur; `PlatformAppService` türet, `[Authorize(...)]` ile koru. 3. **TestCompile** → hatasız → **Publish**. `CompilationStatus` `Success` olmadan yayınlama. 4. Swagger'da `ControllerName` altında göründüğünü doğrula. 5. Ekran tarafında bu servisi ya ListForm'un `*ServiceAddress` alanından ya da Custom Component'in bir data source'undan tüket. ### 10.7 Şablon senaryo — "onaylı talep formu" (avans, izin, masraf, satın alma…) Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep formu istiyorum" dediğinde üretilecek dosya seti: ``` configs/seeds/host/ ├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…) ├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir ├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa ├── wizard/20260826101000_AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, önce └── wizard/20260826101500_AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı ``` **Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise `ComponentKind: 0` (List) yeter ve `custom/` + `crud/` dosyalarına hiç gerek yoktur. Custom Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar) isteniyorsa geçilir. **Adım adım:** 1. **Tablo** — iş kolonları + §8.1 varsayılanları. Onay akışı için dört kolon şarttır: durum (`Status`), onaylayan (`ApproverId`), onay tarihi (`ApprovalDate`), açıklama (`ApprovalNote`). Talep sahibi için de bir kolon (`EmployeeId`) bulunmalıdır. 2. **Wizard dosyası** — §4.4'teki iskelet: - `IsTenant: true` (tabloda `TenantId` var), `IsDeletedField: true`, `IsCreatedField: true` - `MenuParentCode` = modül kökü (`App.Hr`); yoksa wizard yaratır, `Order = max + 1` - `PermissionGroupName` = `App.Hr`, EN + TR görünen adlarıyla - Görünümler: `Grid` + `Card` açık; durum akışı görsel isteniyorsa `Todo: true` + `TodoOptionDto` (§9.8) 3. **Alanlar** — `Groups` içinde iki grup mantıklıdır: "Talep" (tutar, tarih, açıklama) ve "Onay" (durum, onaylayan, onay tarihi, onay notu). Onay grubundaki alanlar `EditorOptions: "{\"readOnly\":true}"` ile kilitlenir; kullanıcı doldurmaz, akış doldurur. 4. **Hesap ve varsayılanlar** — `EditorScript` ile: - Talep tarihi form açılışında bugün: `today` tarifi, tetikleyici `open` - Tutar × oran gibi hesaplar: `multiply` / `percent` - Eşik üstü tutarda uyarı: `notify` ya da `ask`, koşulu `greaterThan` 5. **Onay akışı** — §4.7'deki `WorkflowDto`. Tipik grafik: `Start → Compare (tutar eşiği) → Approval (yönetici) / Approval (üst yönetici) → Inform → End`. `ApprovalIsFilterUserName: true` ile herkes yalnızca kendi onayına düşenleri görür, `ApprovalIsResetWorkflow: true` ile kayıt güncellenince akış başa döner. 6. **Yetki** — wizard `App.Hr.AdvanceRequests` kökünü ve `.Create/.Update/.Delete/.Export/ .Import/.Note` alt yetkilerini üretir. Onaylayanların rolüne kök + `.Update` verilir; talep sahiplerine kök + `.Create` yeter. 7. **Dil** — menü/başlık/açıklama metinleri wizard dosyasındaki `LanguageText*` alanlarından, alan başlıkları her `Items` öğesinin `TurkishCaption`/`EnglishCaption` alanlarından üretilir. Ayrıca `LanguagesData.json`'a dokunmaya gerek yoktur. 8. **Teslim** — dosyaları listele, DB Migrate gerektiğini söyle, §11 kontrol listesini geç. **Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi (alt form olarak masraf kalemleri → menüsüz wizard + `SubForms`), satın alma talebi (kalemler + tedarikçi lookup'ı + iki kademeli onay). --- ## 11. Teslim öncesi kontrol listesi - [ ] Menü, route ve ekran sözleşmesi birbirini gösteriyor. - [ ] Kök yetki + aksiyon yetkileri var; menü `RequiredPermissionName` ile bağlı. - [ ] Yetki grubunun EN + TR görünen adı var. - [ ] Kullanıcıya görünen her metnin EN + TR dil anahtarı var; anahtar tekrar edilmemiş. - [ ] `IsTenant` (ve gerekiyorsa `IsBranch`/`IsOrganizationUnit`) doğru; sorgular tenant güvenli. - [ ] Tüm SQL parametreli; string birleştirme yok. - [ ] `DeleteCommand = DefaultDeleteCommand("{Tablo}")`. - [ ] Seed dosyası oluştu; `InsertedRecords` yalnızca gerçekten yaratılan kayıtları içeriyor. - [ ] Bağımlılıklar (custom component, crud endpoint, sql nesnesi) da seed'li. - [ ] Export zip'i başka bir ortamda açıldığında ekran ayağa kalkıyor. - [ ] Geri alma yolu yazılı: wizard dosyasını sil → `InsertedRecords` temizlenir. Prompt ile üretim yaptıysan ek olarak: - [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`). - [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın zaman damgası daha büyük. - [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında üretildi. - [ ] `KeyFieldDbSourceType`, tablodaki `Id` tipiyle uyumlu. - [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8). - [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde (`designerPermission` / `*Permission` / `checkPermission`). - [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı taşıyor. - [ ] Menü ve route elle eklenmedi; wizard dosyasından geliyor. - [ ] Kullanıcıya hangi dosyaların üretildiği ve DB Migrate gerektiği söylendi. - [ ] Ekran tarifi (§0.6) çıkarıldı ve yol seçimi gerekçelendirildi. - [ ] Görselden üretildiyse: platformda karşılığı olmayan öğeler ve yapılan varsayımlar (tablo/kolon adları, anahtar tipi, lookup kaynakları) açıkça bildirildi. - [ ] Onaycı/iş kuralı gibi görselden okunamayan kritik bilgiler için soru soruldu (§0.7.4).