From 1ede045bace248c55e27d20424c431f2ce0567bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Thu, 8 Oct 2026 01:07:28 +0300 Subject: [PATCH] =?UTF-8?q?EditorScript=20d=C3=BCzenlemeleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/instructions/lowcode.instructions.md | 49 +- README.md | 2 +- .../McpDevelopAppService.cs | 4 +- .../Tools/McpDevelopTools.cs | 2 +- .../ListForms/Wizard/ListFormWizardDto.cs | 20 +- .../ListForms/Wizard/WizardSeedFileDto.cs | 2 +- .../ListForms/ListFormWizardAppService.cs | 9 + .../Editors/EditorScript.cs | 50 ++ .../Editors/EditorScriptCondition.cs | 4 + .../Editors/EditorScriptRule.cs | 84 +++- claude.md | 10 +- .../5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json | 8 +- configs/seeds/data/App.Inventory.Product.json | 19 + .../data/App.Languages.LanguageText.json | 42 ++ configs/seeds/docs/modules/App.Inventory.md | 1 - configs/seeds/wizard/Attribute.json | 6 +- configs/seeds/wizard/DeliveryMethod.json | 12 +- configs/seeds/wizard/Incoterm.json | 14 +- configs/seeds/wizard/PaymentMethod.json | 14 +- configs/seeds/wizard/PaymentTerm.json | 16 +- configs/seeds/wizard/Product.json | 8 +- .../seeds/wizard/ProductAttributeLine.json | 14 +- configs/seeds/wizard/ProductCategory.json | 14 +- configs/seeds/wizard/ProductVariant.json | 428 ++++++------------ configs/seeds/wizard/WorkCenter.json | 6 +- configs/seeds/wizard/WorkCenterTag.json | 14 +- .../scriptBuilder/ScriptBuilderDialog.tsx | 74 ++- ui/src/components/scriptBuilder/types.ts | 5 + ui/src/proxy/admin/wizard/models.ts | 2 + ui/src/utils/editorScriptRuntime.ts | 69 ++- .../edit/form-fields/FormFieldTabOptions.tsx | 13 +- .../EditorScriptBuilderDialog.tsx | 13 +- .../editor-script/formScriptDialect.ts | 18 +- .../editor-script/scriptRecipes.ts | 11 +- ui/src/views/admin/listForm/wizard/Wizard.tsx | 2 + ui/src/views/list/shared/cascade.ts | 12 +- ui/src/views/list/shared/editingForm.ts | 32 +- .../views/list/shared/editingFormScripts.ts | 181 ++++++-- 38 files changed, 837 insertions(+), 447 deletions(-) diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 057e8b49..66c97802 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -546,7 +546,9 @@ eklenen dosya) bu satırlar ekranla birlikte seed edilir; migrate çalıştığ 1. `deploy_wizard` cevabına `SeedData: true` yaz, satırların hepsini aynı çağrıda `seedRows` ile gönder (alan adından değere nesne dizisi). Ekranın `SeedFilePath`'i `data/{ListFormCode}.json` - olur, `SeedSyncInsert/Update/Delete` açılır (§9.15); satırlar deploy anında bu dosyaya yazılır. + olur; satırlar deploy anında bu dosyaya yazılır. `SeedSyncInsert/Update/Delete` (§9.15) demo veriyle + **açılmaz** — varsayılan `false`'tur; yalnızca istek "ekrandan girilen kayıtlar seed dosyasına + yansısın" diyorsa wizard cevabına `SeedSync: true` yazılır (wizard ekranında da aynı kural). `Guid` anahtarı sunucu üretir; `TenantId` ve audit alanlarını migrate doldurur — yazma. Aynı içerikli satır dosyada varsa (tekrar deploy) yeniden eklenmez. 2. DB Migrate'i bekleme. Migrate wizard seeder ile ekranı kurar, ardından `ListFormSeedDataApplier` @@ -1275,7 +1277,8 @@ atanmaz (admin dahil); ekranı görecek roller yetkiyi rol ekranından alır. | --- | --- | | `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. | | `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). | -| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` ve `SeedSyncInsert/Update/Delete` açık üretilir; ekrandan girilen satır dosyaya yazılır, migrate geri yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. | +| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` üretilir; migrate dosyadaki satırları yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. | +| `SeedSync` | `[ListForm]` `SeedData` açıkken `SeedSyncInsert/Update/Delete`'i açar: ekrandan eklenen/değiştirilen/silinen kayıt dosyaya yansır. **Varsayılan `false`**; demo veri bunu açmaz, yalnızca istekte açıkça istenirse `true` yazılır. Okurken üç bayraktan biri açıksa `true`; tek tek ayar `ListForm` bölümünde yapılır. | | `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. **`SubForms` doluysa cevaptan bağımsız `true` üretilir** (`ListFormWizardDto.BuildListForm`): wizard alt formları `Form` tipinde kurar ve sekmeler yalnızca Detay görünümünde açılır. Elle yazılan wizard dosyasında da alt formu olan ekranın `EditingOptionDto.AllowDetail` değeri `true` yazılır. **Alt formu olmayan listede `false` kalır** — MCP/AI üretiminde Detay düğmesi yalnızca `SubForms` taşıyan listeye açılır; kullanıcı açıkça istemedikçe tek tablolu liste ya da alt form ekranının kendisi (`PerformancePeriodSkill` gibi) Detay almaz. | | `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. | | `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). **Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` varsayılan `true`**, diğer dördü `false` (§9.8). Açtığın her görünümün option bloğunu da doldur. | @@ -1876,6 +1879,11 @@ bölümlerine düşer (§4.1). `SeededAt` ilk deploy anında sunucu tarafından > tutarlı** olmalıdır: açılışta çalışan bir kural varsa (hesap tarifleri varsayılan `both`) > `// @runOnOpen` satırı gelir ve metin literalleri çift tırnaklıdır (`JSON.stringify`). Elle yazmak yerine C# tarafında `EditorScript.Build(...)` kullan ya da > Script Builder'da üret; uyuşmazsa dialog script'i "elle düzenlenmiş" sayar. +> **Wizard/MCP deploy'u script'i kurallardan yeniden üretir** (`EditorScript.Rebuild`): `// @builder` +> başlıklı script'in gövdesi atılıp kurallardan yazılır; `EditorScript` alanına yalnızca kural JSON'u +> (`{"v":1,"rules":[…]}`) da verilebilir. MCP script'i elle yazmaz, kuralları verir — elle yazılan +> gövde (yanlış yardımcı kullanımı) çalışmayan script üretiyordu. Başlıksız (tamamen elle yazılmış) +> script olduğu gibi kalır. ### 4.4.1 Deploy'un ve seeder'ın senin yerine ürettikleri — cevaba yazma @@ -2658,7 +2666,7 @@ bağlıdır. | calc | `days` | start, end, target | `set('T', days('Bas', 'Bit'))` — bitiş günü **dahil** | | calc | `hours` | start, end, target | `set('T', hours('Bas', 'Bit'))` — bitiş küçükse ertesi güne taşar | | data | `copy` | source (seçili kayıttaki kolon/yol), target | `copy('unitPrice', 'UnitPrice')` | -| data | `setValue` | target, text | `set('T', tpl('{Musteri} - {value}'))` | +| data | `setValue` | target, text | `set('T', tpl('{Musteri} - {value}'))`; `text` `true`/`false` ise mantıksal sabit yazılır: `set('T', false)` — onay kutusuna metin `'false'` dolu sayılırdı. Runtime da mevcut değeri mantıksal olan alana gelen `'true'/'false'/'1'/'0'`'ı mantıksala çevirir | | data | `clear` | sources[] | `clear('Il', 'Ilce')` | | view | `readOnly` | target, invert? | `readOnly('Alan', )` · invert → `readOnly('Alan', !())` · koşulsuz → `readOnly('Alan', true)` | | interaction | `notify` | message | `notify('Limit aşıldı')` — mesajda `{Alan}` şablonu geçerli | @@ -2726,14 +2734,14 @@ Script `AsyncFunction` ile derlenir, aşağıdaki adlar parametre olarak enjekte | `data` | Script'in üzerinde çalıştığı canlı form verisi (kopya) | | `isReady` | `true` ise script form açılışında çalışıyor | | `get('Alan')` | Form verisinden okur. **Nokta yolu** (`Musteri.Unvan`) ve **büyük/küçük harf duyarsız** eşleşme destekler (SQL kolon adları tutarsız gelebiliyor) | -| `num()` · `str()` · `bool()` | Tip dönüşümü. `bool` için `true`/`'true'`/`1`/`'1'` doğrudur | +| `num()` · `str()` · `bool()` | **Alan adı** alır, değeri değil: `str('ProductType')`. `str(get('X'))` ya da `str(value)` değeri alan adı sanıp boş döner — koşul hiç tutmaz. Değişen alanın değeri için `str(field)` ya da doğrudan `value` kullanılır. `bool` için `true`/`'true'`/`1`/`'1'` doğrudur | | `dateOf('Alan')` | `Date` ya da `null` | | `pick(nesne, 'a.b')` | Herhangi bir nesneden yol okuma | | `set('Alan', deger)` · `set({A:1, B:2})` | Yazar. Değişmeyen değer yazılmaz; yazmalar biriktirilip **tek flush** ile forma ve düzenlenen grid hücresine uygulanır | | `clear('A','B')` | Verilen alanları `null` yapar | | `copy('kolon','Alan')` | Lookup/GridBox'ta **seçili kayıttan** taşır; hedef verilmezse yolun son parçası hedef olur. Seçili kayıt, olay formdan gelse de (popup form, form sayfası) değişen alanın editöründen okunur; lookup sorgusu taşınacak kolonları döndürmelidir (ör. `SELECT Id AS [Key], …, DepartmentId, JobPositionId`) | | `selected('Yol')` · `snum()` · `sstr()` | Seçili kaydın tamamı / sayı / metin hâli | -| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil). Grid popup'ında açılış çalışması (`isReady`) editörler her yeniden çizildiğinde tekrarlanır ve form verisi o anda güncel değildir; koşul başka bir alana bakıyorsa değer `editor.component.cellValue(satır, 'Alan')` ile okunur (örnek: `App.Hr.md` › İzin Yönetimi §5.1) | +| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil). Grid popup'ında açılış çalışması (`isReady`) form her yeniden çizildiğinde (her `cellValue` yazımı) tekrarlanır; veri grid'in düzenlenen satırından güncel okunur (`getEditingRow`), `str('Alan')` yeterlidir | | `round(x, 2)` · `sum('A','B')` | Matematik | | `days('Bas','Bit')` · `hours('Bas','Bit')` | Gün farkı (bitiş dahil) · saat farkı (negatifse +24) | | `tpl('{Alan} - {value}')` | Şablon: `{Alan}`, `{value}`, `{selected.Yol}` | @@ -2750,6 +2758,37 @@ Eski (uzun) script'ler bozulmasın diye geriye dönük uyum vardır: script içi (`formData, e, editor, runtimeSetEditorReadOnly, setFormData`) derlenir ve **yardımcılar tanımsız kalır**. Yardımcı adlarını yeniden tanımlama. +**Zincir ve kalıcı readOnly (Grid/Tree popup).** Script'in `set` ile yazdığı alanın kendi script'i de +çalışır — kullanıcı o alanı değiştirmiş gibi (`runChainedScripts`; derinlik sınırı yoktur, zincir yazılan kadar ilerler; `ProductType` → +`TrackInventory = false` → `TrackingMethod` salt okunur). Grid'e `cellValue` ile yazılan değer editörün +değişiklik olayını tetiklemediği için zincir olmadan bağımlı kural ancak kullanıcı alanı elle değiştirince +çalışırdı. Yazma popup formunu yeniden çizer (`editForm.repaint`) ve açılış script'leri yeniden çalışır; +formun `formData`'sı açılıştaki nesne olduğundan veri grid'in düzenlenen satırından okunur +(`getEditingRow`, `list/shared/editingForm.ts` — yeni satırın anahtarı nesnedir, `row.key ===` eşleşmez). +Eski veriyle çalışan script kendi `readOnly` kararını geri alırdı. Popup formu +`getController('editing')._editForm`'dadır (`getActiveEditingForm`). Form sayfası `updateData` ile alan +olayını zaten tetikler. + +Zincirde bir alanın aynı değerle ikinci kez tetiklenmesi döngüdür (A → B → A) ve durdurulur; değeri her turda +değişen döngüyü 1000 adımlık emniyet sınırı yakalar. İkisi de kullanıcıya hata olarak gösterilir. + +**Hatalı script kullanıcıya gösterilir** — derleme ya da çalışma hatası konsola düşmekle kalmaz, ekranda +`App.EditorScript.Error` bildirimi çıkar (alan adı + hata; `reportEditorScriptError`, +`utils/editorScriptRuntime.ts`). Form her yeniden çizildiğinde aynı hata 5 saniyede bir kez gösterilir. + +**Derlenemeyen script hiç çalışmaz** — bir satırdaki söz dizimi hatası (eksik parantez) diğer kuralları da +durdurur. Script Builder kaydetmeden önce metni derler ve hatada kaydetmez (`App.ScriptBuilder.SyntaxError`). + +**Script Builder ile MCP aynı kodu üretir.** Kod `// @builder` başlığındaki kurallardan üretilir; MCP/wizard +deploy'u gövdeyi atıp kuralları `EditorScript.Rebuild` ile yeniden üretir (C# çıktısı `scriptRecipes.ts` ile +birebir). Onay kutusu/anahtar alanına değer yazan `setValue` kuralında dialog serbest metin yerine Evet/Hayır +sectirir (`getParamChoices`); deploy aynı alana gelen `0`/`1`/`evet`/`hayır` değerini `true`/`false`'a çevirir. +Kod editöründe kurallarla eşleşmeyecek şekilde elle değiştirilen script'te dialog uyarır +(`App.ScriptBuilder.StaleHeader`) ve kaydederken bayat başlığı siler; başlık kalsaydı deploy gövdeyi başlıktan +yeniden üretip elle yapılan düzeltmeyi geri alırdı. +`readOnly` tarifinde `invert: "true"` koşulu tersler: "koşul sağlanmıyorsa salt okunur". "X seçilince kilitle" +için `invert` boş bırakılır. + ### 5.7 Yetenek sınırları — script ile yapılamayanlar - **Alan gizleme yok.** Runtime'da `visible` yardımcısı yoktur; koşullu görünürlük yerine diff --git a/README.md b/README.md index 8273f93c..85805d44 100644 --- a/README.md +++ b/README.md @@ -1563,7 +1563,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır. | `save_sql_object` | View/procedure/function scriptini `sql/object/{Ad}.sql` ve `postgres/object/{Ad}.sql` dosyalarına yazar (`McpSqlObjectWriter`); veritabanında çalıştırılmaz, nesneyi DB Migrate kurar. Gövde diyalektler arasında güvenle çevrilemediği için iki script de istemciden gelir ve aynı nesneyi tanımlamalıdır (`SqlObjectInvalid`); ad scriptten okunur, başlık `CREATE OR ALTER` / `CREATE OR REPLACE`'e çevrilir. Tablo scriptinin üstüne yazmaz. `.Write` + `App.SqlQueryManager` ister. | | `generate_crud_endpoints` | SQL Query Manager CRUD diyaloğuyla aynı endpoint kümesini (`CrudEndpointSeedFileDto.Generate` — diyaloğun servisi de bunu kullanır) yalnızca `crud/{Entity}.json` dosyasına yazar; kayıtları DB Migrate ekler (`CrudEndpointSeedApplier`). `.Write` + `App.SqlQueryManager.CrudEndpoints` ister. | | `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. | -| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …, "ListForm": …, "Fields": […]}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromDeployJson`'dan geçer: Wizard bölümünde yazılan cevap korunur, yazılmayan ekran alanı `ListForm`/`Fields`'tan tamamlanır, iki bölüm de ekrana katman olarak uygulanır (`WizardScreenOverlay`, üç yönlü: bölümdeki değer wizard'ın kendi ürettiğiyle aynıysa cevap kazanır, farklıysa editör ayarıdır ve korunur; kimlik alanları wizard'dan gelir, tanınmayan/hesaplanan anahtar reddedilir). Böylece MCP, ListForm editörünün yapabildiği her ayarı wizard dosyasına yazar ve `replaceFileName` ile güncelleme editör ayarlarını silmez. Yeni kaydın form varsayılanı kolonun tablodaki sabit `DEFAULT`'udur (katalog ya da seed scripti, `SqlSeedColumn.ConstantValue`; IsActive → true, UomId → Adet, para birimi → TRY tablonun `DEFAULT`'larından gelir); alana özel değer öğenin `DefaultValue`'su ile verilir (`lowcode.instructions.md` §4.3.5.1). Girdide sözleşmede olmayan anahtar adıyla reddedilir, sessizce yok sayılmaz. `StaticData` lookup seçeneklerinin düz metni `App.StaticLookup.*` dil anahtarına çevrilir ve EN + TR satırı yazılır (`LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1). Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | +| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …, "ListForm": …, "Fields": […]}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromDeployJson`'dan geçer: Wizard bölümünde yazılan cevap korunur, yazılmayan ekran alanı `ListForm`/`Fields`'tan tamamlanır, iki bölüm de ekrana katman olarak uygulanır (`WizardScreenOverlay`, üç yönlü: bölümdeki değer wizard'ın kendi ürettiğiyle aynıysa cevap kazanır, farklıysa editör ayarıdır ve korunur; kimlik alanları wizard'dan gelir, tanınmayan/hesaplanan anahtar reddedilir). Böylece MCP, ListForm editörünün yapabildiği her ayarı wizard dosyasına yazar ve `replaceFileName` ile güncelleme editör ayarlarını silmez. Yeni kaydın form varsayılanı kolonun tablodaki sabit `DEFAULT`'udur (katalog ya da seed scripti, `SqlSeedColumn.ConstantValue`; IsActive → true, UomId → Adet, para birimi → TRY tablonun `DEFAULT`'larından gelir); alana özel değer öğenin `DefaultValue`'su ile verilir (`lowcode.instructions.md` §4.3.5.1). Girdide sözleşmede olmayan anahtar adıyla reddedilir, sessizce yok sayılmaz. `StaticData` lookup seçeneklerinin düz metni `App.StaticLookup.*` dil anahtarına çevrilir ve EN + TR satırı yazılır (`LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1). Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur (`SeedSync*` bayrakları demo veriyle açılmaz; yalnızca istekte açıkça istenirse wizard'ın `SeedSync: true` cevabıyla) — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). | | `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). | | `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. Sonuç istemcinin 1 MB sınırına sığdırılır: büyük görseller küçültülüp JPEG'e çevrilir (`IMcpImageShrinker`), sığmayan gönderilmez ve metinde bildirilir. `.Write` + `App.DevRequests.Update` ister. | diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index 00e75bce..bfad2feb 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -950,9 +950,9 @@ public class McpDevelopAppService( return (string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json)) ?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } - catch (JsonException ex) + catch (JsonException) { - throw new UserFriendlyException(L[McpErrorCodes.InvalidWizardJson, ex.Message]); + throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs index 0179aa38..76521300 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -97,7 +97,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc => await developAppService.GetWizardAsync(fileName); [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] - [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] + [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. SeedData ekrandan girilen kayitlari dosyaya yansitmaz (SeedSyncInsert/Update/Delete kapali); bunu yalnizca istek acikca isterse SeedSync true yaz. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). EditorScript'i elle yazma: Script Builder kurallarini ({v:1, rules:[...]}, get_dev_guide 5.3) ver, deploy kodu Script Builder dialogu ile birebir ayni uretir; govde yazsan da atilir. Onay kutusu alanina setValue text 'true'/'false' (0/1 cevrilir). set ile yazilan alanin kurallari da sirayla calisir (derinlik siniri yok); A->B->A dongusu kuran kural yazma, calisma aninda hata olarak gosterilir. Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] public async Task DeployWizardAsync( [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}, \"ListForm\": {...}, \"Fields\": [...]}, get_wizard cikisi gibi) ya da duz wizard cevaplari. ListForm/Fields istege baglidir ve kismi olabilir. Enum alanlari sayi olarak yazilir.")] string wizard, diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index 62951c07..73bc42ab 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -100,12 +100,19 @@ public class ListFormWizardDto public bool ConfirmDelete { get; set; } /// - /// Ekranin verisi seed klasorundeki data/{ListFormCode}.json dosyasina aynalanir - /// (SeedFilePath + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar - /// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir. + /// Ekranin demo verisi seed klasorundeki data/{ListFormCode}.json dosyasindan migrate ile + /// yuklenir (SeedFilePath). Ekrandan yapilan kayitlarin dosyaya yansimasi ayri karardir + /// (). /// public bool SeedData { get; set; } + /// + /// Ekrandan yapilan ekle/guncelle/sil islemleri veri dosyasina yansisin mi (SeedSyncInsert/Update/Delete). + /// Demo veri () bunu acmaz: varsayilan kapali, yalnizca istekte acikca + /// istenirse true yazilir; aksi halde kullanicinin girdigi her kayit seed dosyasina karisirdi. + /// + public bool SeedSync { get; set; } + // Liste ekranı varsayılan olarak dört analiz görünümüyle gelir (UI wizard ve LayoutDto ile aynı); // MCP/prompt ile gelen cevapta bayrak yazılmazsa görünüm sessizce kapanmasın diye. public string DefaultLayout { get; set; } = "grid"; @@ -279,9 +286,9 @@ public class ListFormWizardDto // Query'de SelectCommand tablo adi degildir; silme komutu elle yazilir. DeleteCommand = isDeletedField && SelectCommandType != SelectCommandTypeEnum.Query ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null, SeedFilePath = SeedData ? WizardConsts.SeedDataFilePath(listFormCode) : null, - SeedSyncInsert = SeedData, - SeedSyncUpdate = SeedData, - SeedSyncDelete = SeedData, + SeedSyncInsert = SeedData && SeedSync, + SeedSyncUpdate = SeedData && SeedSync, + SeedSyncDelete = SeedData && SeedSync, }; // Varsayilanlar WizardConsts uzerinden JSON olarak kurulur; DTO'nun nesne karsiliklari @@ -450,6 +457,7 @@ public class ListFormWizardDto IsBranch = listForm.IsBranch; IsOrganizationUnit = listForm.IsOrganizationUnit; SeedData = !string.IsNullOrWhiteSpace(listForm.SeedFilePath); + SeedSync = listForm.SeedSyncInsert || listForm.SeedSyncUpdate || listForm.SeedSyncDelete; var layout = listForm.LayoutDto; DefaultLayout = layout.DefaultLayout; diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index 5e9c8292..3a24f147 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -84,7 +84,7 @@ public class WizardSeedFileDto [ nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit), nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting), - nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData), + nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData), nameof(ListFormWizardDto.SeedSync), nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card), nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart), nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo), diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index ef81bfef..bc4a68ff 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -348,6 +348,15 @@ public class ListFormWizardAppService( ValidateImageColumnLengths(input, tableColumns); await LocalizeStaticLookupsAsync(input, fieldDtos, rows, owned, inserted); + // Script Builder kurali tasiyan script'in kodu kurallardan uretilir; MCP'nin elle yazdigi + // govdeye guvenilmez (lowcode §4.4, §5.6). + var booleanFields = fieldDtos + .Where(f => f.SourceDbType == System.Data.DbType.Boolean || f.EditorType2 is "dxCheckBox" or "dxSwitch") + .Select(f => f.FieldName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var field in fieldDtos) + field.EditorScript = Sozsoft.Platform.Editors.EditorScript.Rebuild(field.EditorScript, booleanFields.Contains); + foreach (var item in input.Groups.SelectMany(g => g.Items)) await WriteLanguageRowsAsync(rows, item.CaptionName, item.EnglishCaption, item.TurkishCaption, owned, inserted); } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs index dfd98da1..bf81f805 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs @@ -194,6 +194,56 @@ public static class EditorScript return string.Join("\n", lines); } + /// + /// Script Builder kurallarini tasiyan script'in kodunu kurallardan yeniden uretir. Girdi + /// // @builder {...} basligiyla baslayan bir script ya da yalnizca kural JSON'u + /// ({"v":1,"rules":[...]}) olabilir; basligin altindaki elle yazilmis govde atilir — + /// yanlis yardimci kullanimi (str(get('X'))) calismayan script uretirdi. Kural tasimayan + /// script oldugu gibi doner. verilirse mantiksal alana deger yazan + /// kuralin degeri dialogdaki Evet/Hayir seciminin karsiligina (true/false) cevrilir. + /// + public static string Rebuild(string script, System.Func isBooleanField = null) + { + var text = script?.Trim(); + if (string.IsNullOrEmpty(text)) + { + return script; + } + + string json; + if (text.StartsWith(BuilderHeader, System.StringComparison.Ordinal)) + { + var lineEnd = text.IndexOf('\n'); + json = (lineEnd < 0 ? text : text[..lineEnd])[BuilderHeader.Length..]; + } + else if (text.StartsWith('{')) + { + json = text; + } + else + { + return script; + } + + try + { + using var document = System.Text.Json.JsonDocument.Parse(json); + if (!document.RootElement.TryGetProperty("rules", out var rules) || rules.ValueKind != System.Text.Json.JsonValueKind.Array) + { + return script; + } + + var built = Build(rules.EnumerateArray() + .Select(EditorScriptRule.FromJson) + .Select(rule => isBooleanField == null ? rule : rule.WithBooleanValue(isBooleanField))); + return built.Length == 0 ? script : built; + } + catch (System.Text.Json.JsonException) + { + return script; + } + } + /* ---------------------------------------------------------------- * * Ortak yardimcilar (scriptRecipes.ts portu) * ---------------------------------------------------------------- */ diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptCondition.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptCondition.cs index ea1a9459..20f9a90b 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptCondition.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptCondition.cs @@ -45,6 +45,10 @@ public sealed class EditorScriptCondition internal string Value { get; } + /// Kural JSON'undaki kosulu geri okur (). + internal static EditorScriptCondition Of(string @operator, string source, string value) + => new EditorScriptCondition(@operator, source, value); + /// Kosulsuz - kural her zaman calisir. public static EditorScriptCondition Always() => new EditorScriptCondition("always", string.Empty, string.Empty); diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs index 2e490b5e..5b48b6b9 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs @@ -114,6 +114,76 @@ public sealed class EditorScriptRule return this; } + /// + /// ciktisini (dialogun @builder basligindaki kural) geri okur; bilinmeyen + /// tarif 'ten gecmez ve uretimde atlanir. + /// + internal static EditorScriptRule FromJson(System.Text.Json.JsonElement json) + { + static string Text(System.Text.Json.JsonElement element, string name) => + element.ValueKind == System.Text.Json.JsonValueKind.Object && element.TryGetProperty(name, out var value) + ? value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.ToString() + : null; + + static EditorScriptCondition Condition(System.Text.Json.JsonElement element) => + EditorScriptCondition.Of(Text(element, "operator") ?? "always", Text(element, "source"), Text(element, "value")); + + var rule = new EditorScriptRule(Text(json, "recipe") ?? string.Empty).WithId(Text(json, "id")); + + if (json.TryGetProperty("params", out var parameters) && parameters.ValueKind == System.Text.Json.JsonValueKind.Object) + { + foreach (var parameter in parameters.EnumerateObject()) + rule.Set(parameter.Name, Text(parameters, parameter.Name)); + } + + if (json.TryGetProperty("fields", out var fields) && fields.ValueKind == System.Text.Json.JsonValueKind.Array) + rule.SetFields(fields.EnumerateArray().Select(field => field.ToString())); + + var conditions = new List(); + if (json.TryGetProperty("condition", out var condition)) + conditions.Add(Condition(condition)); + if (json.TryGetProperty("conditions", out var more) && more.ValueKind == System.Text.Json.JsonValueKind.Array) + conditions.AddRange(more.EnumerateArray().Select(Condition)); + + // Kosulsuz kural "always" ile yazilir; ToJson onu kendisi ekler. + rule.AddConditions([.. conditions.Where(c => c.Operator != "always")], Text(json, "join") == "or"); + + return rule.WithTrigger(Text(json, "trigger") switch + { + "change" => EditorScriptTrigger.Change, + "open" => EditorScriptTrigger.Open, + "both" => EditorScriptTrigger.Both, + _ => EditorScriptTrigger.Default + }); + } + + /// + /// Mantiksal alana yazan setValue kuralinin degerini true/false'a cevirir; dialog bu + /// alanlarda yalnizca Evet/Hayir sectirir. tpl("0") metni onay kutusunu dolu sayardi. + /// + internal EditorScriptRule WithBooleanValue(System.Func isBooleanField) + { + if (_recipe != "setValue" || !isBooleanField(Param("target"))) + { + return this; + } + + var flag = Param("text").Trim().ToLowerInvariant() switch + { + "true" or "1" or "yes" or "evet" or "on" => "true", + "false" or "0" or "no" or "hayir" or "hayır" or "off" => "false", + _ => null + }; + + var index = _params.FindIndex(pair => pair.Key == "text"); + if (flag != null && index >= 0) + { + _params[index] = new KeyValuePair("text", flag); + } + + return this; + } + /// Tek kurallik script metni. Birden fazla kural icin kullanin. public string ToScript() => EditorScript.Build(this); @@ -352,7 +422,7 @@ public sealed class EditorScriptRule return "copy(" + q(Param("source")) + ", " + q(Param("target")) + ")"; case "setValue": - return "set(" + q(Param("target")) + ", tpl(" + q(Param("text")) + "))"; + return "set(" + q(Param("target")) + ", " + ValueLiteral(Param("text")) + ")"; case "clear": return "clear(" + string.Join(", ", _fields.Select(q)) + ")"; @@ -396,6 +466,18 @@ public sealed class EditorScriptRule } } + /// + /// true/false mantiksal sabit olarak yazilir; metin olarak (tpl('false')) yazilsa + /// onay kutusu dolu sayardi. Diger degerler sablondur. scriptRecipes.ts valueLiteral ile ayni. + /// + private static string ValueLiteral(string text) + { + var trimmed = (text ?? string.Empty).Trim(); + return trimmed.Equals("true", System.StringComparison.OrdinalIgnoreCase) || trimmed.Equals("false", System.StringComparison.OrdinalIgnoreCase) + ? trimmed.ToLowerInvariant() + : "tpl(" + EditorScript.Quote(text) + ")"; + } + private static string TriggerName(EditorScriptTrigger trigger) { switch (trigger) diff --git a/claude.md b/claude.md index 6560b198..befe8a25 100644 --- a/claude.md +++ b/claude.md @@ -232,6 +232,13 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `name` düz metin değil `App.StaticLookup.*` dil anahtarıdır; EN + TR satırı dil dosyasına yazılır. Deploy düz metni kendisi anahtara çevirir (aynı TR metinli anahtar varsa onu kullanır) — `ListFormWizardAppService.LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1. +20. **EditorScript** → MCP/wizard script'i elle yazmaz, Script Builder kurallarını verir; deploy kodu + kurallardan üretir (`EditorScript.Rebuild`). `str/num/bool` alan **adı** alır (`str('X')`, değeri + değil). `setValue`'da `true`/`false` mantıksal yazılır (onay kutusu alanında `0`/`1` deploy'da çevrilir). + MCP ve Script Builder aynı kuralları aynı koda çevirir; `set` zinciri derinlik sınırsızdır, döngü ve + script hatası kullanıcıya bildirilir. Ayrıntı: `lowcode.instructions.md` §4.4, §5.3, §5.6. +21. **Demo veri ≠ seed senkronu** → `SeedData: true` yalnızca `SeedFilePath` açar; + `SeedSyncInsert/Update/Delete` istekte açıkça istenmedikçe `false`'tur (wizard cevabı `SeedSync`). ### Cross-cutting: touching one thing means touching these too @@ -335,7 +342,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → adım ekleme. Ayrıntı: `lowcode.instructions.md` §4.0. - **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json` - + `SeedSync*` açık, `ListFormWizardDto.BuildListForm`) ve satırlar aynı çağrıda `seedRows` ile + ; `SeedSync*` bayrakları **kapalı** kalır — yalnızca istek ekrandan girilen kayıtların dosyaya + yansımasını açıkça isterse `SeedSync: true`, wizard ekranında da aynı; `ListFormWizardDto.BuildListForm`) ve satırlar aynı çağrıda `seedRows` ile `configs/seeds/data/{ListFormCode}.json` dosyasına yazılır (`McpDevelopAppService`; alan adları ekrana göre doğrulanır, `Guid` anahtarı sunucu üretir). Migrate ekranı kurar, ardından satırları ekler; MCP migrate'i beklemez. Talep döngüsü `Testing`'de biter, `Staging`'e kullanıcı alır. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz. diff --git a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json index b7d82a10..15949f4a 100644 --- a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json +++ b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json @@ -9,18 +9,18 @@ "TenantId": null, "UserName": "system@sozsoft.com", "RequestDate": null, - "Status": "Ready to Start", + "Status": "Staging", "Title": "Inventory -> Tanımlamalar", "Description": "

Ürünler : Ürün listesi ve detayı aşağıdaki örneklendirilmiştir. Ürünler Satış, Masraflar, Satınalma olarak 3 seçeneği var. Satış seçerse sadece Satış siparişlerinde görünür. Satınalma seçerse Satınalınabilir anlamına gelir. Ürün Türü Mal veya Hizmet olabilir. Envanter Takibi yapılıp yapılmayacağı seçilebilir. Eğer takip edilecekse Seri No, Lot Numarası veya Miktara göre seklinden seçilecek.


Nitelikler ve Varyantlar seçeneğinde yazılabilecek değerler Nitelik olarak tanımlanmalı. İçerisindeki değerleri de tanımlanabilir. Nitelik seçilip değerleri birden fazla şekilde yapılabilir. Eğer nitelikli bir ürün tanımlanırsa o varyantlı ürün listeler de seçilecektir.


Satınalma kısmında o ürünü hangi tedarikçiden hangi bedele aldığımızı belirtiyoruz. Ürün ve reçetede kullanılan Birim sütunun Adm_T_Uom tablosu ile ilişkili olmalı. Adm_T_Uom tablosundaki Id bilgisi nvarchar(64)



Ürün listesinde ürün adı, resmi, Satış, Masraflar ve Satınalma sütunlarından oluşmalı. Ayrıca yukarıdaki Ürün lisesindeki sütunlar olabilir. Diğer bilgiler Ürün listesindeki Detay ksımında Tablar şeklinde resimdeki gibi görüntülenmeli. App.Definitions.Contact gibi olabilir.



Nitelikler kısmı Nitelik kısmında seçilmeli ve Değerler birden fazla seçebilecek bir sütundan oluşmalı.




Reçeteler : Ürünlerin hammadde ve yarı mamülerin ve tüketim miktarlarının belirlendiği kısımdır. Ayrıca tanımlanan operasyonların tanımlandığı listedir. Hangi bileşenlerin hangi operasyon ile tüketildiği belirlenecektir. Ürün bazında da reçete yazılabilir. Her bir ürünün Varyantı bazında da reçete yazılabilri. Bom türü eklenmeyecek


İş Merkezi tanımlamaları : Inventory -> Tanımlamalar menüsünde yer alacaktır.İş Merkezi lsitesinde iş merkezi adı, etiketler, çalışma saatleri (Weekly working time limit (h) ile setting kısmından alınabilir.)İş Merkezi lsitesinde iş merkezi adı, etiketler, çalışma saatleri (Weekly working time limit (h) ile setting kısmından alınabilir.) İş merkezi lsitesinin detayında Genel Bİlgi, Ürün kapasitesi bilgilerini tab olarak göstermeli


Operasyonlar : Inventory -> Tanımlamalar menüsünde yer alacaktır.Operasyon Listesinde Operasyon Adı, İş Merkezi Adı, Varsayılan Süre sütunlarından oluşmalı. Detay butonu ile Açıklamalar listesinde yer almalı



Eğer nitelikli bir ürün tanımlanırsa o zaman Varyantlı liste oluşur ve tüm sistem varyantlı seçeneker çalışır. Eğer nitelik tanımlamazsa ana ürün seçilecektir.

Ürün reçetesine genel bakış raporu bu ERP ssitemlerdeki BOM ile aynı. Reçetelerin malzemelerinde recursive şekilde iç içe kullanılan diğer malzemeler ve miktarların hesaplanması gerekiyor. Bu ksıım Tree Layout olarak sunulmalı.




", "DueDate": "2026-10-06T21:02:00", "Tags": "IN", "Assignees": "system@sozsoft.com", "Priority": "5", - "Completed": false, - "SortOrder": 0, + "Completed": true, + "SortOrder": 7, "Timesheets": "[{\"id\":\"09379560-7e9f-fa43-dfd3-3a2429dc2afe\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0167}]", "SubTasks": null, - "Analysis": "

1. Amaç

\n

Envanter ve üretim modülünün ana verisini kurmak: ürün kartı (satış/masraf/satınalma bayrakları, tür, envanter takibi, nitelik–varyant, tedarikçi fiyatları), ölçü birimi bağlı reçeteler (bileşen + operasyon, ürün ya da varyant bazında), iş merkezleri, operasyonlar ve çok seviyeli reçete patlatma raporu. Kullanıcı: stok, satınalma ve üretim planlama ekipleri.

\n\n

2. Görsel analizi

\n
    \n
  • [image-1] Ürün listesi (grid): favori yıldız, Ürün Adı, İç Referans, Satış Fiyatı, Maliyet, Stokta, Öngörülen, Birim. Arama + filtre çubuğu.
  • \n
  • [image-2] Nitelik formu: Nitelik Adı, Görünüm Türü (Radio/Haplar/Seçme/Renk/Çoklu onay/Görsel), Varyant Oluşturma (Hemen/Dinamik/Asla); alt sekme \"Nitelik Değerleri\" liste: Değer, Serbest metin (bit), Varsayılan Ekstra Fiyat. Demo: renk → kırmızı, mavi.
  • \n
  • [image-3] Ürün formu – Genel Bilgi: başlıkta Satış / Masraflar / Satınalma onay kutuları + resim; Ürün Türü (Mallar/Hizmet/Kombo), Envanter Takibi (bit) + takip yöntemi (Seri No'ya göre / Lotlara göre / Miktara göre), Satış Fiyatı + birim, Satış Vergileri, Maliyet, Satınalma Vergileri, Kategori, Referans, Barkod, GTİP, Firma; İç Notlar.
  • \n
  • [image-4] Nitelik & Varyantlar sekmesi: satır listesi Nitelik (seçim) + Değerler (çoklu).
  • \n
  • [image-5] Satınalma sekmesi: Tedarikçi, Miktar, Birim, Birim Fiyat, Kur, Teslim Süresi; altta Kontrol Kuralı (Sipariş edilen / Teslim alınan miktarlar), Satınalma Açıklaması.
  • \n
  • [image-6] Stok sekmesi: Sorumlu, Ağırlık (kg), Hacim (m³), GTİP Kodu, Malların Menşei; Lot/Seri bazında değerleme, Özel Lot/Seri; Alım / Teslimat / İç transfer açıklamaları.
  • \n
  • [image-7] Muhasebe sekmesi: Gelir Hesabı, Gider Hesabı.
  • \n
  • [image-8..10] Reçete formu: Ürün, Ürün Varyantı, Miktar + birim, Referans, (BoM Türü – talep gereği eklenmez), Firma. Sekmeler: Bileşenler (Komponent, Miktar, Birim, Operasyonda Tüketilenler), Operasyonlar (Operasyon, İş Merkezi, Süre dk), Diğer (Üretim Hazırlığı, Esnek Tüketim, Üretim Akış Zamanı, Sipariş Hazırlama Günleri, Parti Büyüklüğü).
  • \n
  • [image-11] İş merkezi formu: İş Merkezi Adı, Etiket, Alternatif İş Merkezleri, Kod, Çalışma Saatleri; sekmeler Genel Bilgi (Zaman Verimliliği %, OEE Hedefi %, Kurulum/Temizlik Zamanı dk, Saat Başına Maliyet iş merkezi/çalışan, Açıklama), Ürün Kapasitesi.
  • \n
  • [image-12] Operasyon formu: Operasyon adı, İş Merkezi, Varyantlara Uygula, Maliyete Dayalı (Gerçek/Teorik), Süre Hesaplama (Sabit/Hesaplanmış), Varsayılan Süre (dk); sekme Açıklamalar (Başlık, Tip, Referans).
  • \n
  • [image-13] Ürün Varyantları listesi: Ürün adı + varyant değerleri (renk/malzeme/boyut etiketleri), Satış Fiyatı, Maliyet, Birim. 12 satır (2×2×3 kartezyen).
  • \n
  • [image-14] Reçeteye Genel Bakış: ürün → bileşenler (girintili), Operasyonlar grubu; Miktar, Birim, Maliyet kolonları.
  • \n
\n\n

3. Çözüm yolu

\n

Tüm tanım ve kart ekranları Wizard List + alt formlar (Contact kartındaki gibi; tek kaydı düzenleyen sekmeler aynı tabloya bağlı Form alt formu). Varyant listesi ve reçete patlatma raporu Query kaynaklı Wizard List (SelectCommandType: 4): varyantlar nitelik satırlarından kartezyen üretilir, rapor özyinelemeli CTE ile çok seviyeli patlatılır ve Tree görünümünde sunulur. Kalıcı varyant tablosu ve SP gerekmez; tüm adımlar create_table + deploy_wizard ile yapılır.

\n\n

4. Menü yeri

\n

Envanter › Tanımlamalar › {Ekran}

\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.InventoryEnvanter / InventoryFcPackageYeni
AraApp.Inventory.DefinitionsTanımlamalar / DefinitionsFcSettingsYeni
EkranApp.Inventory.ProductCategoryÜrün Kategorileri / Product CategoriesFcFolderYeni
EkranApp.Inventory.AttributeNitelikler / AttributesFcListYeni
EkranApp.Inventory.WorkCenterTagİş Merkezi Etiketleri / Work Center TagsFcBookmarkYeni
EkranApp.Inventory.WorkCenterİş Merkezleri / Work CentersFcFactoryYeni
EkranApp.Inventory.OperationOperasyonlar / OperationsFcProcessYeni
EkranApp.Inventory.ProductÜrünler / ProductsFcShopYeni
EkranApp.Inventory.ProductVariantÜrün Varyantları / Product VariantsFcGridYeni
EkranApp.Inventory.BomReçeteler / Bills of MaterialsFcDocumentYeni
EkranApp.Inventory.BomOverviewReçeteye Genel Bakış / BoM OverviewFcTreeStructureYeni
\n

ModuleId: Inventory · İzin grubu: App.Inventory (Yeni) · Route: /admin/list/{ListFormCode}. Kök veya Tanımlamalar dalı menü seed'inde aynı adla varsa deploy var olana bağlar.

\n

Yetki: App.Inventory › App.Inventory.Definitions (Yeni) › {ListFormCode}; alt formlar kendi listelerinin altında (ör. … › App.Inventory.Product › App.Inventory.ProductSupplier).

\n\n

5. Veri modeli

\n

Tümü kiracı (TenantId) + audit + soft delete (IsDeleted) kolonlu, anahtar Id uniqueidentifier. Birim kolonları UomId nvarchar(64) → Adm_T_Uom.Id; tedarikçi Adm_T_Contact; para birimi Sas_H_Currency.

\n

Inv_T_ProductCategory — Tanım tablosu: Evet (görselde Kategori seçimi). Name nvarchar(128) zorunlu, ParentId uniqueidentifier null (→ kendisi), IsActive bit NOT NULL DEFAULT 1.

\n

Inv_T_Attribute — Tanım tablosu: Evet (talep: \"Nitelik olarak tanımlanmalı\"). Name nvarchar(128) zorunlu, DisplayType nvarchar(16) zorunlu (Radio/Pills/Select/Color/Multi/Image, varsayılan Radio), VariantCreation nvarchar(16) zorunlu (Always/Dynamic/Never, varsayılan Always), IsActive bit.

\n

Inv_T_AttributeValue — Tanım tablosu: Evet. AttributeId uniqueidentifier zorunlu, Name nvarchar(128) zorunlu, IsCustom bit (Serbest metin) DEFAULT 0, ExtraPrice decimal(18,2) DEFAULT 0, ColorCode nvarchar(16) null, SortOrder int, IsActive bit.

\n

Inv_T_WorkCenterTag — Tanım: Evet. Name nvarchar(64) zorunlu, Color nvarchar(16), IsActive bit.

\n

Inv_T_WorkCenter — Tanım: Evet (operasyon ve reçetede lookup kaynağı). Name nvarchar(128) zorunlu, Code nvarchar(32), Tags nvarchar(1000) (dxTagBox, | ayrımlı Inv_T_WorkCenterTag Id), AlternativeWorkCenterIds nvarchar(2000) (dxTagBox), WeeklyWorkingHours decimal(5,2) zorunlu, TimeEfficiency decimal(5,2) DEFAULT 100, OeeTarget decimal(5,2) DEFAULT 90, SetupTimeMin decimal(9,2) DEFAULT 0, CleanupTimeMin decimal(9,2) DEFAULT 0, CostPerHour decimal(18,2) DEFAULT 0, EmployeeCostPerHour decimal(18,2) DEFAULT 0, Description nvarchar(MAX), IsActive bit.

\n

Inv_T_WorkCenterCapacity — Tanım: Hayır. WorkCenterId zorunlu, ProductId uniqueidentifier zorunlu, UomId nvarchar(64), Capacity decimal(18,4) zorunlu, SetupTimeMin decimal(9,2), CleanupTimeMin decimal(9,2).

\n

Inv_T_Operation — Tanım: Evet (reçete operasyonlarının kaynağı). Name nvarchar(128) zorunlu, WorkCenterId zorunlu, ApplyOnVariants nvarchar(4000) (dxTagBox, AttributeValue Id), CostBasis nvarchar(16) (Actual/Theoretical, varsayılan Actual), DurationMode nvarchar(16) (Fixed/Computed, varsayılan Fixed), DefaultDurationMin decimal(9,2) DEFAULT 60, IsActive bit.

\n

Inv_T_OperationInstruction — Tanım: Hayır. OperationId zorunlu, Title nvarchar(256) zorunlu, InstructionType nvarchar(16) (Text/Pdf/Image/Link), Reference nvarchar(1000), Note nvarchar(MAX), SortOrder int.

\n

Inv_T_Product — Tanım: Evet (tüm modüllerin ürün kaynağı). Name nvarchar(256) zorunlu, Image nvarchar(MAX) (dxImageUpload), IsFavorite bit DEFAULT 0, CanBeSold bit DEFAULT 1, CanBeExpensed bit DEFAULT 0, CanBePurchased bit DEFAULT 1, ProductType nvarchar(16) zorunlu (Wares/Service, varsayılan Wares), TrackInventory bit DEFAULT 0, TrackingMethod nvarchar(16) null (Serial/Lot/Quantity), InternalReference nvarchar(64), Barcode nvarchar(64), GtipCode nvarchar(32), CategoryId uniqueidentifier, UomId nvarchar(64) zorunlu, PurchaseUomId nvarchar(64), SalesPrice decimal(18,4) DEFAULT 0, Cost decimal(18,4) DEFAULT 0, SalesTaxRate decimal(5,2) DEFAULT 20, PurchaseTaxRate decimal(5,2) DEFAULT 20, InternalNotes nvarchar(MAX), ResponsibleUserId uniqueidentifier (→ AbpUsers), Weight decimal(18,4), Volume decimal(18,4), OriginCountryId (→ Sas_H_Country), LotValuation bit DEFAULT 0, CustomLotSerial nvarchar(64), ReceiptNote / DeliveryNote / InternalTransferNote nvarchar(MAX), PurchaseControlPolicy nvarchar(16) (Ordered/Received, varsayılan Received), PurchaseDescription nvarchar(MAX), IncomeAccount nvarchar(64), ExpenseAccount nvarchar(64), IsActive bit.

\n

Inv_T_ProductAttributeLine — Tanım: Hayır. ProductId zorunlu, AttributeId zorunlu, ValueIds nvarchar(4000) zorunlu (dxTagBox, | ayrımlı AttributeValue Id; seçili niteliğe göre süzülür). Benzersizlik: (ProductId, AttributeId).

\n

Inv_T_ProductSupplier — Tanım: Hayır. ProductId zorunlu, ContactId zorunlu (→ Adm_T_Contact), MinQuantity decimal(18,4) DEFAULT 1, UomId nvarchar(64), Price decimal(18,4) zorunlu, CurrencyId (→ Sas_H_Currency), LeadTimeDays int DEFAULT 0, VariantKey nvarchar(450) null.

\n

Inv_T_Bom — Tanım: Evet (bileşen olarak alt reçete aranır). ProductId zorunlu, VariantKey nvarchar(450) null (boşsa tüm varyantlar), Quantity decimal(18,4) DEFAULT 1 zorunlu, UomId nvarchar(64) zorunlu, Reference nvarchar(64), ReadyToProduce nvarchar(16) (AllAvailable/FirstOperation), FlexibleConsumption nvarchar(16) (Allowed/Warning/Blocked, varsayılan Warning), ProductionLeadDays int DEFAULT 0, PreparationDays int DEFAULT 0, BatchSize decimal(18,4) null, IsActive bit. BoM Türü kolonu açılmaz (talep).

\n

Inv_T_BomComponent — Tanım: Hayır. BomId zorunlu, ProductId zorunlu, VariantKey nvarchar(450) null, Quantity decimal(18,4) zorunlu, UomId nvarchar(64) zorunlu, BomOperationId uniqueidentifier null (Operasyonda Tüketilenler), SortOrder int.

\n

Inv_T_BomOperation — Tanım: Hayır. BomId zorunlu, OperationId zorunlu, WorkCenterId zorunlu (operasyondan dolar), DurationMin decimal(9,2) zorunlu, SortOrder int.

\n

Varyant anahtarı: varyant kalıcı tabloda tutulmaz. VariantKey = ürünün nitelik satırlarındaki seçili değer Id'lerinin sıralı | birleşimi; aynı kombinasyon her zaman aynı anahtarı üretir. Query kaynakları (T‑SQL):

\n
    \n
  • App.Inventory.ProductVariant (TableName \"Q\", KeyFieldName Key = ProductId + ':' + VariantKey, nvarchar): nitelik satırlarını STRING_SPLIT(ValueIds,'|') ile açıp özyinelemeli CTE ile kartezyen üretir; kolonlar ProductId, ProductName, VariantKey, VariantName (değer adları virgülle), SalesPrice + Σ ExtraPrice, Cost, UomId, InternalReference. Nitelik satırı olmayan ürün tek satır (VariantKey boş). Ana tablo Inv_T_Product IsDeleted seçilir.
  • \n
  • App.Inventory.BomOverview (KeyFieldName NodeKey nvarchar(900)): aktif Inv_T_Bom reçetelerinden başlayan özyinelemeli CTE; her bileşen için o ürün(+varyant, yoksa ürün geneli) reçetesi varsa alt seviyeye iner. Miktar = üst miktar × bileşen miktarı ÷ reçete miktarı; Maliyet = miktar × Product.Cost; operasyonlar her seviyede \"Operasyonlar\" düğümü altında DurationMin × (miktar ÷ reçete miktarı) ve WorkCenter.CostPerHour ile. Döngüye karşı OPTION (MAXRECURSION 20) ve yol (Path) içinde aynı BomId tekrarı engellenir. Kolonlar NodeKey, ParentKey, Level, NodeType (Product/Component/OperationGroup/Operation), Name, Quantity, UomName, Cost.
  • \n
\n\n

6. Alt form ağacı

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SekmeHedef ListFormCodeTabTypeMenüİlişkiEkle / Düzenle / SilYetki yolu
Nitelik DeğerleriApp.Inventory.AttributeValueListMenüsüzId → AttributeId✓ / ✓ / ✓… › Attribute › AttributeValue
İş Merkezi › Genel BilgiApp.Inventory.WorkCenterGeneral (Inv_T_WorkCenter)FormMenüsüzId → Id✓ / ✓ / –… › WorkCenter › WorkCenterGeneral
İş Merkezi › Ürün KapasitesiApp.Inventory.WorkCenterCapacityListMenüsüzId → WorkCenterId✓ / ✓ / ✓… › WorkCenter › WorkCenterCapacity
Operasyon › AçıklamalarApp.Inventory.OperationInstructionListMenüsüzId → OperationId✓ / ✓ / ✓… › Operation › OperationInstruction
Ürün › Genel BilgiApp.Inventory.ProductGeneral (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductGeneral
Ürün › Nitelik & VaryantlarApp.Inventory.ProductAttributeLineListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductAttributeLine
Ürün › VaryantlarApp.Inventory.ProductVariantListMenülüId → ProductId– / – / –kendi dalı
Ürün › SatınalmaApp.Inventory.ProductSupplierListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductSupplier
Ürün › StokApp.Inventory.ProductStock (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductStock
Ürün › MuhasebeApp.Inventory.ProductAccounting (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductAccounting
Ürün › ReçetelerApp.Inventory.BomListMenülüId → ProductId✓ / ✓ / ✓kendi dalı
Reçete › BileşenlerApp.Inventory.BomComponentListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomComponent
Reçete › OperasyonlarApp.Inventory.BomOperationListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomOperation
Reçete › DiğerApp.Inventory.BomOther (Inv_T_Bom)FormMenüsüzId → Id✓ / ✓ / –… › Bom › BomOther
Reçete › Genel BakışApp.Inventory.BomOverviewListMenülüId → BomId– / – / –kendi dalı
\n\n

7. Etkilenen kısımlar

\n
    \n
  • Yeni kök menü Envanter, ara menü Tanımlamalar, izin grubu App.Inventory, 9 menülü ekran + 12 menüsüz alt form; her listede C/R/U/D/Export/Import/Note yetkileri (Query ekranlarında yalnız R/Export).
  • \n
  • Dil anahtarları (TR/EN) her ekran, alan ve statik seçenek için.
  • \n
  • Modül dokümanı docs/modules/App.Inventory.md deploy ile açılır.
  • \n
  • Var olan ekranlara dokunulmaz; Adm_T_Uom, Adm_T_Contact, Sas_H_Currency, Sas_H_Country yalnız lookup kaynağıdır. Sipariş ekranları ürünü CanBePurchased/CanBeSold ile süzecek; bu talepte sipariş ekranı yoktur.
  • \n
\n\n

8. Görünümler ve davranış

\n
    \n
  • Grid/Card/Pivot/Chart açık (standart). BomOverview varsayılan Tree (KeyExpr NodeKey, ParentIdExpr ParentKey, AutoExpandAll true), Grid de açık.
  • \n
  • Ürün listesi kolonları: Image, Name, CanBeSold, CanBeExpensed, CanBePurchased, InternalReference, SalesPrice, Cost, UomId, CategoryId, ProductType. Stokta/Öngörülen kolonları stok hareket tablosu olmadığından açılmaz.
  • \n
  • Ürün ana formu: Name, Image (dxImageUpload), Satış/Masraflar/Satınalma onay kutuları, ProductType (dxRadioGroup), TrackInventory, TrackingMethod (dxRadioGroup), UomId, SalesPrice, Cost, CategoryId. Diğer alanlar sekmelerde.
  • \n
  • EditorScript: TrackInventory = false iken TrackingMethod gizli ve null; ProductType = Service iken TrackInventory false ve kilitli. Varsayılan TrackingMethod = Quantity.
  • \n
  • ProductAttributeLine.ValueIds lookup'ı seçili AttributeId'ye göre cascade süzülür (dxTagBox).
  • \n
  • Bom.VariantKey ve BomComponent.VariantKey: lookup App.Inventory.ProductVariant sorgusundan, seçili ProductId'ye göre cascade; ürünün nitelik satırı yoksa boş kalır (ana ürün). BomComponent.BomOperationId aynı reçetenin operasyonlarıyla süzülür. BomOperation.OperationId seçilince WorkCenterId ve DurationMin operasyondan dolar.
  • \n
  • Product.UomId seçilince PurchaseUomId boşsa aynı değeri alır; BomComponent.UomId ürünün UomId'sinden dolar.
  • \n
  • WorkCenter.WeeklyWorkingHours varsayılanı (FormFieldsDefaultValueJson, DbQuery): SELECT COALESCE(MAX(TRY_CAST(Value AS decimal(5,2))),40) FROM AbpSettings WHERE Name LIKE '%WeeklyWorkingTimeLimit%'.
  • \n
  • Doğrulamalar: Quantity/Capacity/Price > 0; OEE ve verimlilik 0–100; TrackInventory = true iken TrackingMethod zorunlu.
  • \n
  • Widget'lar yalnız ana ekranlarda: Ürünler (toplam, satılabilir, satınalınabilir, hizmet), Reçeteler (toplam, varyant bazlı), İş Merkezleri (toplam, aktif). Alt formlarda widget yok.
  • \n
\n\n

9. Kararlar

\n
    \n
  • Ara menü kodu App.Inventory.Definitions (Satınalmadaki App.Prc.Definitions kalıbı).
  • \n
  • Ürün Kategorileri ve İş Merkezi Etiketleri tanım ekranları eklendi — görsellerdeki Kategori ve Etiket alanlarının lookup kaynağı.
  • \n
  • Ürün Türü yalnız Mal / Hizmet (talep); Kombo açılmaz.
  • \n
  • Vergiler vergi tablosu olmadığından oran (decimal, varsayılan %20) olarak tutulur (onay).
  • \n
  • Muhasebe hesapları hesap planı tablosu olmadığından serbest metin kod (nvarchar(64)) (onay).
  • \n
  • Varyantlar kalıcı tabloya yazılmaz; deterministik VariantKey ile Query ekranından türetilir. Silinen kombinasyonu kullanan reçete satırı reçete listesinde kırmızı biçimlendirilir (ColumnStyling) (onay).
  • \n
  • \"Varyant Oluşturma = Asla\" olan nitelik kartezyene katılmaz; Dinamik ve Hemen aynı davranır (onay).
  • \n
  • Çalışma saati ayardan varsayılan olarak okunur, kayıt bazında değiştirilebilir; ayar yoksa 40 (onay).
  • \n
  • Reçete patlatmada alt reçete seçimi: önce bileşenin VariantKey'ine uyan, yoksa VariantKey boş reçete; birden fazla aktif reçete varsa en eski oluşturulan.
  • \n
  • Demo veri görsellerden: nitelikler renk (kırmızı, mavi), malzeme (çelik, tahta), boyut (40x40, 80x100, 10x30); ürünler \"(40X40mm) Mamül Sepeti Üst Geçme Kepi\", \"(K.M.G) AUTONİCS PR12-4DN Sensör\", \"(K.M.G) 2A Cam Sigorta (5*20)\"; iş merkezi \"ram makinası\"; operasyon \"Apre\" (60 dk); reçete 1 adet kep → 20 adet sensör.
  • \n
\n\n

10. Üretim planı

\n
    \n
  1. create_table: Inv_T_ProductCategory
  2. \n
  3. create_table: Inv_T_Attribute
  4. \n
  5. create_table: Inv_T_AttributeValue
  6. \n
  7. create_table: Inv_T_WorkCenterTag
  8. \n
  9. create_table: Inv_T_WorkCenter
  10. \n
  11. create_table: Inv_T_WorkCenterCapacity
  12. \n
  13. create_table: Inv_T_Operation
  14. \n
  15. create_table: Inv_T_OperationInstruction
  16. \n
  17. create_table: Inv_T_Product
  18. \n
  19. create_table: Inv_T_ProductAttributeLine
  20. \n
  21. create_table: Inv_T_ProductSupplier
  22. \n
  23. create_table: Inv_T_Bom
  24. \n
  25. create_table: Inv_T_BomComponent
  26. \n
  27. create_table: Inv_T_BomOperation
  28. \n
  29. deploy_wizard: ProductCategory.json
  30. \n
  31. deploy_wizard: AttributeValue.json
  32. \n
  33. deploy_wizard: Attribute.json
  34. \n
  35. deploy_wizard: WorkCenterTag.json
  36. \n
  37. deploy_wizard: WorkCenterGeneral.json
  38. \n
  39. deploy_wizard: WorkCenterCapacity.json
  40. \n
  41. deploy_wizard: WorkCenter.json
  42. \n
  43. deploy_wizard: OperationInstruction.json
  44. \n
  45. deploy_wizard: Operation.json
  46. \n
  47. deploy_wizard: ProductVariant.json
  48. \n
  49. deploy_wizard: BomComponent.json
  50. \n
  51. deploy_wizard: BomOperation.json
  52. \n
  53. deploy_wizard: BomOther.json
  54. \n
  55. deploy_wizard: BomOverview.json
  56. \n
  57. deploy_wizard: Bom.json
  58. \n
  59. deploy_wizard: ProductGeneral.json
  60. \n
  61. deploy_wizard: ProductAttributeLine.json
  62. \n
  63. deploy_wizard: ProductSupplier.json
  64. \n
  65. deploy_wizard: ProductStock.json
  66. \n
  67. deploy_wizard: ProductAccounting.json
  68. \n
  69. deploy_wizard: Product.json
  70. \n
\n\n

11. Üretime hazır

\n

Evet

" + "Analysis": "

1. Amaç

\n

Envanter ve üretim modülünün ana verisini kurmak: ürün kartı (satış/masraf/satınalma bayrakları, tür, envanter takibi, nitelik–varyant, tedarikçi fiyatları), ölçü birimi bağlı reçeteler (bileşen + operasyon, ürün ya da varyant bazında), iş merkezleri, operasyonlar ve çok seviyeli reçete patlatma raporu. Kullanıcı: stok, satınalma ve üretim planlama ekipleri.

\n\n

2. Görsel analizi

\n
    \n
  • [image-1] Ürün listesi (grid): favori yıldız, Ürün Adı, İç Referans, Satış Fiyatı, Maliyet, Stokta, Öngörülen, Birim. Arama + filtre çubuğu.
  • \n
  • [image-2] Nitelik formu: Nitelik Adı, Görünüm Türü (Radio/Haplar/Seçme/Renk/Çoklu onay/Görsel), Varyant Oluşturma (Hemen/Dinamik/Asla); alt sekme \"Nitelik Değerleri\" liste: Değer, Serbest metin (bit), Varsayılan Ekstra Fiyat. Demo: renk → kırmızı, mavi.
  • \n
  • [image-3] Ürün formu – Genel Bilgi: başlıkta Satış / Masraflar / Satınalma onay kutuları + resim; Ürün Türü (Mallar/Hizmet/Kombo), Envanter Takibi (bit) + takip yöntemi (Seri No'ya göre / Lotlara göre / Miktara göre), Satış Fiyatı + birim, Satış Vergileri, Maliyet, Satınalma Vergileri, Kategori, Referans, Barkod, GTİP, Firma; İç Notlar.
  • \n
  • [image-4] Nitelik & Varyantlar sekmesi: satır listesi Nitelik (seçim) + Değerler (çoklu).
  • \n
  • [image-5] Satınalma sekmesi: Tedarikçi, Miktar, Birim, Birim Fiyat, Kur, Teslim Süresi; altta Kontrol Kuralı (Sipariş edilen / Teslim alınan miktarlar), Satınalma Açıklaması.
  • \n
  • [image-6] Stok sekmesi: Sorumlu, Ağırlık (kg), Hacim (m³), GTİP Kodu, Malların Menşei; Lot/Seri bazında değerleme, Özel Lot/Seri; Alım / Teslimat / İç transfer açıklamaları.
  • \n
  • [image-7] Muhasebe sekmesi: Gelir Hesabı, Gider Hesabı.
  • \n
  • [image-8..10] Reçete formu: Ürün, Ürün Varyantı, Miktar + birim, Referans, (BoM Türü – talep gereği eklenmez), Firma. Sekmeler: Bileşenler (Komponent, Miktar, Birim, Operasyonda Tüketilenler), Operasyonlar (Operasyon, İş Merkezi, Süre dk), Diğer (Üretim Hazırlığı, Esnek Tüketim, Üretim Akış Zamanı, Sipariş Hazırlama Günleri, Parti Büyüklüğü).
  • \n
  • [image-11] İş merkezi formu: İş Merkezi Adı, Etiket, Alternatif İş Merkezleri, Kod, Çalışma Saatleri; sekmeler Genel Bilgi (Zaman Verimliliği %, OEE Hedefi %, Kurulum/Temizlik Zamanı dk, Saat Başına Maliyet iş merkezi/çalışan, Açıklama), Ürün Kapasitesi.
  • \n
  • [image-12] Operasyon formu: Operasyon adı, İş Merkezi, Varyantlara Uygula, Maliyete Dayalı (Gerçek/Teorik), Süre Hesaplama (Sabit/Hesaplanmış), Varsayılan Süre (dk); sekme Açıklamalar (Başlık, Tip, Referans).
  • \n
  • [image-13] Ürün Varyantları listesi: Ürün adı + varyant değerleri (renk/malzeme/boyut etiketleri), Satış Fiyatı, Maliyet, Birim. 12 satır (2×2×3 kartezyen).
  • \n
  • [image-14] Reçeteye Genel Bakış: ürün → bileşenler (girintili), Operasyonlar grubu; Miktar, Birim, Maliyet kolonları.
  • \n
\n\n

3. Çözüm yolu

\n

Tüm tanım ve kart ekranları Wizard List + alt formlar (Contact kartındaki gibi; tek kaydı düzenleyen sekmeler aynı tabloya bağlı Form alt formu). Varyant listesi ve reçete patlatma raporu Query kaynaklı Wizard List (SelectCommandType: 4): varyantlar nitelik satırlarından kartezyen üretilir, rapor özyinelemeli CTE ile çok seviyeli patlatılır ve Tree görünümünde sunulur. Kalıcı varyant tablosu ve SP gerekmez; tüm adımlar create_table + deploy_wizard ile yapılır.

\n\n

4. Menü yeri

\n

Envanter › Tanımlamalar › {Ekran}

\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.InventoryEnvanter / InventoryFcPackageYeni
AraApp.Inventory.DefinitionsTanımlamalar / DefinitionsFcSettingsYeni
EkranApp.Inventory.ProductCategoryÜrün Kategorileri / Product CategoriesFcFolderYeni
EkranApp.Inventory.AttributeNitelikler / AttributesFcListYeni
EkranApp.Inventory.WorkCenterTagİş Merkezi Etiketleri / Work Center TagsFcBookmarkYeni
EkranApp.Inventory.WorkCenterİş Merkezleri / Work CentersFcFactoryYeni
EkranApp.Inventory.OperationOperasyonlar / OperationsFcProcessYeni
EkranApp.Inventory.ProductÜrünler / ProductsFcShopYeni
EkranApp.Inventory.ProductVariantÜrün Varyantları / Product VariantsFcGridYeni
EkranApp.Inventory.BomReçeteler / Bills of MaterialsFcDocumentYeni
EkranApp.Inventory.BomOverviewReçeteye Genel Bakış / BoM OverviewFcTreeStructureYeni
\n

ModuleId: Inventory · İzin grubu: App.Inventory (Yeni) · Route: /admin/list/{ListFormCode}. Kök veya Tanımlamalar dalı menü seed'inde aynı adla varsa deploy var olana bağlar.

\n

Yetki: App.Inventory › App.Inventory.Definitions (Yeni) › {ListFormCode}; alt formlar kendi listelerinin altında (ör. … › App.Inventory.Product › App.Inventory.ProductSupplier).

\n\n

5. Veri modeli

\n

Tümü kiracı (TenantId) + audit + soft delete (IsDeleted) kolonlu, anahtar Id uniqueidentifier. Birim kolonları UomId nvarchar(64) → Adm_T_Uom.Id; tedarikçi Adm_T_Contact; para birimi Sas_H_Currency.

\n

Inv_T_ProductCategory — Tanım tablosu: Evet (görselde Kategori seçimi). Name nvarchar(128) zorunlu, ParentId uniqueidentifier null (→ kendisi), IsActive bit NOT NULL DEFAULT 1.

\n

Inv_T_Attribute — Tanım tablosu: Evet (talep: \"Nitelik olarak tanımlanmalı\"). Name nvarchar(128) zorunlu, DisplayType nvarchar(16) zorunlu (Radio/Pills/Select/Color/Multi/Image, varsayılan Radio), VariantCreation nvarchar(16) zorunlu (Always/Dynamic/Never, varsayılan Always), IsActive bit.

\n

Inv_T_AttributeValue — Tanım tablosu: Evet. AttributeId uniqueidentifier zorunlu, Name nvarchar(128) zorunlu, IsCustom bit (Serbest metin) DEFAULT 0, ExtraPrice decimal(18,2) DEFAULT 0, ColorCode nvarchar(16) null, SortOrder int, IsActive bit.

\n

Inv_T_WorkCenterTag — Tanım: Evet. Name nvarchar(64) zorunlu, Color nvarchar(16), IsActive bit.

\n

Inv_T_WorkCenter — Tanım: Evet (operasyon ve reçetede lookup kaynağı). Name nvarchar(128) zorunlu, Code nvarchar(32), Tags nvarchar(1000) (dxTagBox, | ayrımlı Inv_T_WorkCenterTag Id), AlternativeWorkCenterIds nvarchar(2000) (dxTagBox), WeeklyWorkingHours decimal(5,2) zorunlu, TimeEfficiency decimal(5,2) DEFAULT 100, OeeTarget decimal(5,2) DEFAULT 90, SetupTimeMin decimal(9,2) DEFAULT 0, CleanupTimeMin decimal(9,2) DEFAULT 0, CostPerHour decimal(18,2) DEFAULT 0, EmployeeCostPerHour decimal(18,2) DEFAULT 0, Description nvarchar(MAX), IsActive bit.

\n

Inv_T_WorkCenterCapacity — Tanım: Hayır. WorkCenterId zorunlu, ProductId uniqueidentifier zorunlu, UomId nvarchar(64), Capacity decimal(18,4) zorunlu, SetupTimeMin decimal(9,2), CleanupTimeMin decimal(9,2).

\n

Inv_T_Operation — Tanım: Evet (reçete operasyonlarının kaynağı). Name nvarchar(128) zorunlu, WorkCenterId zorunlu, ApplyOnVariants nvarchar(4000) (dxTagBox, AttributeValue Id), CostBasis nvarchar(16) (Actual/Theoretical, varsayılan Actual), DurationMode nvarchar(16) (Fixed/Computed, varsayılan Fixed), DefaultDurationMin decimal(9,2) DEFAULT 60, IsActive bit.

\n

Inv_T_OperationInstruction — Tanım: Hayır. OperationId zorunlu, Title nvarchar(256) zorunlu, InstructionType nvarchar(16) (Text/Pdf/Image/Link), Reference nvarchar(1000), Note nvarchar(MAX), SortOrder int.

\n

Inv_T_Product — Tanım: Evet (tüm modüllerin ürün kaynağı). Name nvarchar(256) zorunlu, Image nvarchar(MAX) (dxImageUpload), IsFavorite bit DEFAULT 0, CanBeSold bit DEFAULT 1, CanBeExpensed bit DEFAULT 0, CanBePurchased bit DEFAULT 1, ProductType nvarchar(16) zorunlu (Goods/Service, varsayılan Goods), TrackInventory bit DEFAULT 0, TrackingMethod nvarchar(16) null (Serial/Lot/Quantity), InternalReference nvarchar(64), Barcode nvarchar(64), GtipCode nvarchar(32), CategoryId uniqueidentifier, UomId nvarchar(64) zorunlu, PurchaseUomId nvarchar(64), SalesPrice decimal(18,4) DEFAULT 0, Cost decimal(18,4) DEFAULT 0, SalesTaxRate decimal(5,2) DEFAULT 20, PurchaseTaxRate decimal(5,2) DEFAULT 20, InternalNotes nvarchar(MAX), ResponsibleUserId uniqueidentifier (→ AbpUsers), Weight decimal(18,4), Volume decimal(18,4), OriginCountryId (→ Sas_H_Country), LotValuation bit DEFAULT 0, CustomLotSerial nvarchar(64), ReceiptNote / DeliveryNote / InternalTransferNote nvarchar(MAX), PurchaseControlPolicy nvarchar(16) (Ordered/Received, varsayılan Received), PurchaseDescription nvarchar(MAX), IncomeAccount nvarchar(64), ExpenseAccount nvarchar(64), IsActive bit.

\n

Inv_T_ProductAttributeLine — Tanım: Hayır. ProductId zorunlu, AttributeId zorunlu, ValueIds nvarchar(4000) zorunlu (dxTagBox, | ayrımlı AttributeValue Id; seçili niteliğe göre süzülür). Benzersizlik: (ProductId, AttributeId).

\n

Inv_T_ProductSupplier — Tanım: Hayır. ProductId zorunlu, ContactId zorunlu (→ Adm_T_Contact), MinQuantity decimal(18,4) DEFAULT 1, UomId nvarchar(64), Price decimal(18,4) zorunlu, CurrencyId (→ Sas_H_Currency), LeadTimeDays int DEFAULT 0, VariantKey nvarchar(450) null.

\n

Inv_T_Bom — Tanım: Evet (bileşen olarak alt reçete aranır). ProductId zorunlu, VariantKey nvarchar(450) null (boşsa tüm varyantlar), Quantity decimal(18,4) DEFAULT 1 zorunlu, UomId nvarchar(64) zorunlu, Reference nvarchar(64), ReadyToProduce nvarchar(16) (AllAvailable/FirstOperation), FlexibleConsumption nvarchar(16) (Allowed/Warning/Blocked, varsayılan Warning), ProductionLeadDays int DEFAULT 0, PreparationDays int DEFAULT 0, BatchSize decimal(18,4) null, IsActive bit. BoM Türü kolonu açılmaz (talep).

\n

Inv_T_BomComponent — Tanım: Hayır. BomId zorunlu, ProductId zorunlu, VariantKey nvarchar(450) null, Quantity decimal(18,4) zorunlu, UomId nvarchar(64) zorunlu, BomOperationId uniqueidentifier null (Operasyonda Tüketilenler), SortOrder int.

\n

Inv_T_BomOperation — Tanım: Hayır. BomId zorunlu, OperationId zorunlu, WorkCenterId zorunlu (operasyondan dolar), DurationMin decimal(9,2) zorunlu, SortOrder int.

\n

Varyant anahtarı: varyant kalıcı tabloda tutulmaz. VariantKey = ürünün nitelik satırlarındaki seçili değer Id'lerinin sıralı | birleşimi; aynı kombinasyon her zaman aynı anahtarı üretir. Query kaynakları (T‑SQL):

\n
    \n
  • App.Inventory.ProductVariant (TableName \"Q\", KeyFieldName Key = ProductId + ':' + VariantKey, nvarchar): nitelik satırlarını STRING_SPLIT(ValueIds,'|') ile açıp özyinelemeli CTE ile kartezyen üretir; kolonlar ProductId, ProductName, VariantKey, VariantName (değer adları virgülle), SalesPrice + Σ ExtraPrice, Cost, UomId, InternalReference. Nitelik satırı olmayan ürün tek satır (VariantKey boş). Ana tablo Inv_T_Product IsDeleted seçilir.
  • \n
  • App.Inventory.BomOverview (KeyFieldName NodeKey nvarchar(900)): aktif Inv_T_Bom reçetelerinden başlayan özyinelemeli CTE; her bileşen için o ürün(+varyant, yoksa ürün geneli) reçetesi varsa alt seviyeye iner. Miktar = üst miktar × bileşen miktarı ÷ reçete miktarı; Maliyet = miktar × Product.Cost; operasyonlar her seviyede \"Operasyonlar\" düğümü altında DurationMin × (miktar ÷ reçete miktarı) ve WorkCenter.CostPerHour ile. Döngüye karşı OPTION (MAXRECURSION 20) ve yol (Path) içinde aynı BomId tekrarı engellenir. Kolonlar NodeKey, ParentKey, Level, NodeType (Product/Component/OperationGroup/Operation), Name, Quantity, UomName, Cost.
  • \n
\n\n

6. Alt form ağacı

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
SekmeHedef ListFormCodeTabTypeMenüİlişkiEkle / Düzenle / SilYetki yolu
Nitelik DeğerleriApp.Inventory.AttributeValueListMenüsüzId → AttributeId✓ / ✓ / ✓… › Attribute › AttributeValue
İş Merkezi › Genel BilgiApp.Inventory.WorkCenterGeneral (Inv_T_WorkCenter)FormMenüsüzId → Id✓ / ✓ / –… › WorkCenter › WorkCenterGeneral
İş Merkezi › Ürün KapasitesiApp.Inventory.WorkCenterCapacityListMenüsüzId → WorkCenterId✓ / ✓ / ✓… › WorkCenter › WorkCenterCapacity
Operasyon › AçıklamalarApp.Inventory.OperationInstructionListMenüsüzId → OperationId✓ / ✓ / ✓… › Operation › OperationInstruction
Ürün › Genel BilgiApp.Inventory.ProductGeneral (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductGeneral
Ürün › Nitelik & VaryantlarApp.Inventory.ProductAttributeLineListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductAttributeLine
Ürün › VaryantlarApp.Inventory.ProductVariantListMenülüId → ProductId– / – / –kendi dalı
Ürün › SatınalmaApp.Inventory.ProductSupplierListMenüsüzId → ProductId✓ / ✓ / ✓… › Product › ProductSupplier
Ürün › StokApp.Inventory.ProductStock (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductStock
Ürün › MuhasebeApp.Inventory.ProductAccounting (Inv_T_Product)FormMenüsüzId → Id✓ / ✓ / –… › Product › ProductAccounting
Ürün › ReçetelerApp.Inventory.BomListMenülüId → ProductId✓ / ✓ / ✓kendi dalı
Reçete › BileşenlerApp.Inventory.BomComponentListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomComponent
Reçete › OperasyonlarApp.Inventory.BomOperationListMenüsüzId → BomId✓ / ✓ / ✓… › Bom › BomOperation
Reçete › DiğerApp.Inventory.BomOther (Inv_T_Bom)FormMenüsüzId → Id✓ / ✓ / –… › Bom › BomOther
Reçete › Genel BakışApp.Inventory.BomOverviewListMenülüId → BomId– / – / –kendi dalı
\n\n

7. Etkilenen kısımlar

\n
    \n
  • Yeni kök menü Envanter, ara menü Tanımlamalar, izin grubu App.Inventory, 9 menülü ekran + 12 menüsüz alt form; her listede C/R/U/D/Export/Import/Note yetkileri (Query ekranlarında yalnız R/Export).
  • \n
  • Dil anahtarları (TR/EN) her ekran, alan ve statik seçenek için.
  • \n
  • Modül dokümanı docs/modules/App.Inventory.md deploy ile açılır.
  • \n
  • Var olan ekranlara dokunulmaz; Adm_T_Uom, Adm_T_Contact, Sas_H_Currency, Sas_H_Country yalnız lookup kaynağıdır. Sipariş ekranları ürünü CanBePurchased/CanBeSold ile süzecek; bu talepte sipariş ekranı yoktur.
  • \n
\n\n

8. Görünümler ve davranış

\n
    \n
  • Grid/Card/Pivot/Chart açık (standart). BomOverview varsayılan Tree (KeyExpr NodeKey, ParentIdExpr ParentKey, AutoExpandAll true), Grid de açık.
  • \n
  • Ürün listesi kolonları: Image, Name, CanBeSold, CanBeExpensed, CanBePurchased, InternalReference, SalesPrice, Cost, UomId, CategoryId, ProductType. Stokta/Öngörülen kolonları stok hareket tablosu olmadığından açılmaz.
  • \n
  • Ürün ana formu: Name, Image (dxImageUpload), Satış/Masraflar/Satınalma onay kutuları, ProductType (dxRadioGroup), TrackInventory, TrackingMethod (dxRadioGroup), UomId, SalesPrice, Cost, CategoryId. Diğer alanlar sekmelerde.
  • \n
  • EditorScript: TrackInventory = false iken TrackingMethod gizli ve null; ProductType = Service iken TrackInventory false ve kilitli. Varsayılan TrackingMethod = Quantity.
  • \n
  • ProductAttributeLine.ValueIds lookup'ı seçili AttributeId'ye göre cascade süzülür (dxTagBox).
  • \n
  • Bom.VariantKey ve BomComponent.VariantKey: lookup App.Inventory.ProductVariant sorgusundan, seçili ProductId'ye göre cascade; ürünün nitelik satırı yoksa boş kalır (ana ürün). BomComponent.BomOperationId aynı reçetenin operasyonlarıyla süzülür. BomOperation.OperationId seçilince WorkCenterId ve DurationMin operasyondan dolar.
  • \n
  • Product.UomId seçilince PurchaseUomId boşsa aynı değeri alır; BomComponent.UomId ürünün UomId'sinden dolar.
  • \n
  • WorkCenter.WeeklyWorkingHours varsayılanı (FormFieldsDefaultValueJson, DbQuery): SELECT COALESCE(MAX(TRY_CAST(Value AS decimal(5,2))),40) FROM AbpSettings WHERE Name LIKE '%WeeklyWorkingTimeLimit%'.
  • \n
  • Doğrulamalar: Quantity/Capacity/Price > 0; OEE ve verimlilik 0–100; TrackInventory = true iken TrackingMethod zorunlu.
  • \n
  • Widget'lar yalnız ana ekranlarda: Ürünler (toplam, satılabilir, satınalınabilir, hizmet), Reçeteler (toplam, varyant bazlı), İş Merkezleri (toplam, aktif). Alt formlarda widget yok.
  • \n
\n\n

9. Kararlar

\n
    \n
  • Ara menü kodu App.Inventory.Definitions (Satınalmadaki App.Prc.Definitions kalıbı).
  • \n
  • Ürün Kategorileri ve İş Merkezi Etiketleri tanım ekranları eklendi — görsellerdeki Kategori ve Etiket alanlarının lookup kaynağı.
  • \n
  • Ürün Türü yalnız Mal / Hizmet (talep); Kombo açılmaz.
  • \n
  • Vergiler vergi tablosu olmadığından oran (decimal, varsayılan %20) olarak tutulur (onay).
  • \n
  • Muhasebe hesapları hesap planı tablosu olmadığından serbest metin kod (nvarchar(64)) (onay).
  • \n
  • Varyantlar kalıcı tabloya yazılmaz; deterministik VariantKey ile Query ekranından türetilir. Silinen kombinasyonu kullanan reçete satırı reçete listesinde kırmızı biçimlendirilir (ColumnStyling) (onay).
  • \n
  • \"Varyant Oluşturma = Asla\" olan nitelik kartezyene katılmaz; Dinamik ve Hemen aynı davranır (onay).
  • \n
  • Çalışma saati ayardan varsayılan olarak okunur, kayıt bazında değiştirilebilir; ayar yoksa 40 (onay).
  • \n
  • Reçete patlatmada alt reçete seçimi: önce bileşenin VariantKey'ine uyan, yoksa VariantKey boş reçete; birden fazla aktif reçete varsa en eski oluşturulan.
  • \n
  • Demo veri görsellerden: nitelikler renk (kırmızı, mavi), malzeme (çelik, tahta), boyut (40x40, 80x100, 10x30); ürünler \"(40X40mm) Mamül Sepeti Üst Geçme Kepi\", \"(K.M.G) AUTONİCS PR12-4DN Sensör\", \"(K.M.G) 2A Cam Sigorta (5*20)\"; iş merkezi \"ram makinası\"; operasyon \"Apre\" (60 dk); reçete 1 adet kep → 20 adet sensör.
  • \n
\n\n

10. Üretim planı

\n
    \n
  1. create_table: Inv_T_ProductCategory
  2. \n
  3. create_table: Inv_T_Attribute
  4. \n
  5. create_table: Inv_T_AttributeValue
  6. \n
  7. create_table: Inv_T_WorkCenterTag
  8. \n
  9. create_table: Inv_T_WorkCenter
  10. \n
  11. create_table: Inv_T_WorkCenterCapacity
  12. \n
  13. create_table: Inv_T_Operation
  14. \n
  15. create_table: Inv_T_OperationInstruction
  16. \n
  17. create_table: Inv_T_Product
  18. \n
  19. create_table: Inv_T_ProductAttributeLine
  20. \n
  21. create_table: Inv_T_ProductSupplier
  22. \n
  23. create_table: Inv_T_Bom
  24. \n
  25. create_table: Inv_T_BomComponent
  26. \n
  27. create_table: Inv_T_BomOperation
  28. \n
  29. deploy_wizard: ProductCategory.json
  30. \n
  31. deploy_wizard: AttributeValue.json
  32. \n
  33. deploy_wizard: Attribute.json
  34. \n
  35. deploy_wizard: WorkCenterTag.json
  36. \n
  37. deploy_wizard: WorkCenterGeneral.json
  38. \n
  39. deploy_wizard: WorkCenterCapacity.json
  40. \n
  41. deploy_wizard: WorkCenter.json
  42. \n
  43. deploy_wizard: OperationInstruction.json
  44. \n
  45. deploy_wizard: Operation.json
  46. \n
  47. deploy_wizard: ProductVariant.json
  48. \n
  49. deploy_wizard: BomComponent.json
  50. \n
  51. deploy_wizard: BomOperation.json
  52. \n
  53. deploy_wizard: BomOther.json
  54. \n
  55. deploy_wizard: BomOverview.json
  56. \n
  57. deploy_wizard: Bom.json
  58. \n
  59. deploy_wizard: ProductGeneral.json
  60. \n
  61. deploy_wizard: ProductAttributeLine.json
  62. \n
  63. deploy_wizard: ProductSupplier.json
  64. \n
  65. deploy_wizard: ProductStock.json
  66. \n
  67. deploy_wizard: ProductAccounting.json
  68. \n
  69. deploy_wizard: Product.json
  70. \n
\n\n

11. Üretime hazır

\n

Evet

" } ] } diff --git a/configs/seeds/data/App.Inventory.Product.json b/configs/seeds/data/App.Inventory.Product.json index 55e34ef1..ba17b732 100644 --- a/configs/seeds/data/App.Inventory.Product.json +++ b/configs/seeds/data/App.Inventory.Product.json @@ -53,6 +53,25 @@ "Cost": 0, "IsActive": true, "Id": "4de8b328-a0bf-3b36-96d9-3a2429f022bf" + }, + { + "Id": "eb0ce687-3014-4f8b-9482-469f3e6d3993", + "Name": "Deneme", + "IsFavorite": false, + "CanBeSold": true, + "CanBeExpensed": false, + "CanBePurchased": false, + "ProductType": "Service", + "CategoryId": "eb9eed73-a7e1-5b5b-c31b-3a2429dfe211", + "TrackInventory": false, + "TrackingMethod": "Quantity", + "SalesPrice": 0.0000, + "Cost": 0.0000, + "InternalReference": null, + "UomId": "Adet", + "PurchaseUomId": "Adet", + "IsActive": true, + "Image": null } ] } diff --git a/configs/seeds/data/App.Languages.LanguageText.json b/configs/seeds/data/App.Languages.LanguageText.json index 67169e51..c8f5d7b8 100644 --- a/configs/seeds/data/App.Languages.LanguageText.json +++ b/configs/seeds/data/App.Languages.LanguageText.json @@ -59742,6 +59742,48 @@ "Key": "App.ScriptBuilder.AddOption", "Value": "+ ekle" }, + { + "Id": "92147417-0362-5de6-9fdd-8c45da7e3857", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ScriptBuilder.SyntaxError", + "Value": "The script has a syntax error and was not saved: {message}" + }, + { + "Id": "cd1e1fe4-af2d-5042-b258-ae16236da342", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ScriptBuilder.SyntaxError", + "Value": "Script'te söz dizimi hatası var, kaydedilmedi: {message}" + }, + { + "Id": "e0f30407-7383-5a15-ad1d-7beb2c8a264c", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.EditorScript.Error", + "Value": "The editor script of field '{field}' is faulty and did not run: {message}" + }, + { + "Id": "cf351e16-422f-565e-ae84-ff72de0f8102", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.EditorScript.Error", + "Value": "'{field}' alanının editör script'i hatalı, çalışmadı: {message}" + }, + { + "Id": "cf45e07a-0a2e-50bc-92d2-af9850cd22d7", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ScriptBuilder.StaleHeader", + "Value": "The code no longer matches the wizard rules. On save the rules header is removed and the code is kept as hand-written; use Regenerate to return to the rules." + }, + { + "Id": "7c7d420a-df72-5d21-b2e4-5f9f95bfe873", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ScriptBuilder.StaleHeader", + "Value": "Kod sihirbaz kurallarıyla artık eşleşmiyor. Kaydedince kural başlığı kaldırılır ve kod elle yazılmış olarak kalır; kurallara dönmek için Yeniden üret'i kullanın." + }, { "Id": "5c33f8a3-775e-50ba-9e9b-97e0a4032e7a", "CultureName": "en", diff --git a/configs/seeds/docs/modules/App.Inventory.md b/configs/seeds/docs/modules/App.Inventory.md index 73d9dc09..86c4205d 100644 --- a/configs/seeds/docs/modules/App.Inventory.md +++ b/configs/seeds/docs/modules/App.Inventory.md @@ -252,7 +252,6 @@ Dış entegrasyon yok. Ölçü Birimleri, Kontaklar, Para Birimleri, Ülkeler ve | `InternalReference` | İç Referans | dxTextBox | | ✓ | | | `ProductName` | Adı | dxTextBox | | ✓ | | | `VariantName` | Varyant Değerleri | dxTextBox | | ✓ | | -| `VariantKey` | Varyant Anahtarı | dxTextBox | | | | | `SalesPrice` | Satış Fiyatı | dxNumberBox | | ✓ | | | `Cost` | Maliyet | dxNumberBox | | ✓ | | | `UomId` | Birim | dxSelectBox | | ✓ | sorgu: Adm_T_Uom | diff --git a/configs/seeds/wizard/Attribute.json b/configs/seeds/wizard/Attribute.json index 180f9693..93bfa80d 100644 --- a/configs/seeds/wizard/Attribute.json +++ b/configs/seeds/wizard/Attribute.json @@ -82,9 +82,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Inventory.Attribute.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Inv_T_Attribute\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, diff --git a/configs/seeds/wizard/DeliveryMethod.json b/configs/seeds/wizard/DeliveryMethod.json index 51dc521d..21b47496 100644 --- a/configs/seeds/wizard/DeliveryMethod.json +++ b/configs/seeds/wizard/DeliveryMethod.json @@ -70,9 +70,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Prc.DeliveryMethod.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Prc_T_DeliveryMethod\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -771,7 +771,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -909,7 +909,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1047,7 +1047,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/Incoterm.json b/configs/seeds/wizard/Incoterm.json index 7c7c0079..a5684fa9 100644 --- a/configs/seeds/wizard/Incoterm.json +++ b/configs/seeds/wizard/Incoterm.json @@ -76,9 +76,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Prc.Incoterm.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Prc_T_Incoterm\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -777,7 +777,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -915,7 +915,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1053,7 +1053,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1191,7 +1191,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/PaymentMethod.json b/configs/seeds/wizard/PaymentMethod.json index 6141d533..d2c61d73 100644 --- a/configs/seeds/wizard/PaymentMethod.json +++ b/configs/seeds/wizard/PaymentMethod.json @@ -76,9 +76,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Prc.PaymentMethod.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Prc_T_PaymentMethod\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -777,7 +777,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -915,7 +915,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1041,7 +1041,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1179,7 +1179,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/PaymentTerm.json b/configs/seeds/wizard/PaymentTerm.json index cf5858e5..0b610a8a 100644 --- a/configs/seeds/wizard/PaymentTerm.json +++ b/configs/seeds/wizard/PaymentTerm.json @@ -82,9 +82,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Prc.PaymentTerm.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Prc_T_PaymentTerm\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -783,7 +783,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -921,7 +921,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1047,7 +1047,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1185,7 +1185,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, @@ -1311,7 +1311,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 4, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/Product.json b/configs/seeds/wizard/Product.json index d7a14297..b20dbf31 100644 --- a/configs/seeds/wizard/Product.json +++ b/configs/seeds/wizard/Product.json @@ -171,9 +171,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Inventory.Product.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Inv_T_Product\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -1846,7 +1846,7 @@ "GridBoxOptions": null, "TagBoxOptions": null, "ImageUploadOptions": null, - "EditorScript": "// @runOnOpen\nreadOnly(\"TrackInventory\", str(get(\"ProductType\")) === \"Service\")\nif (!isReady && str(value) === \"Service\") { set(\"TrackInventory\", false); clear(\"TrackingMethod\"); readOnly(\"TrackingMethod\", true) }", + "EditorScript": "// @runOnOpen\nif (str(\"ProductType\") === \"Service\") set(\"TrackInventory\", false)\nreadOnly(\"TrackInventory\", str(\"ProductType\") === \"Service\")", "LastModificationTime": null, "LastModifierId": null, "CreationTime": "0001-01-01T00:00:00", diff --git a/configs/seeds/wizard/ProductAttributeLine.json b/configs/seeds/wizard/ProductAttributeLine.json index 556d5c51..e30a5818 100644 --- a/configs/seeds/wizard/ProductAttributeLine.json +++ b/configs/seeds/wizard/ProductAttributeLine.json @@ -763,7 +763,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -889,7 +889,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -1027,7 +1027,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1121,11 +1121,11 @@ "DataSourceType": 2, "ValueExpr": "Key", "DisplayExpr": "Name", - "CascadeRelationField": null, + "CascadeRelationField": "", "CascadeFilterOperator": "=", "CascadeParentFields": "AttributeId", - "CascadeEmptyFields": null, - "LookupQuery": "SELECT CAST(Id AS nvarchar(36)) AS [Key], Name AS [Name] FROM Inv_T_AttributeValue WHERE IsDeleted = 'false' AND IsActive = 1 AND (AttributeId = @param0 OR @param0 IS NULL) ORDER BY SortOrder, Name" + "CascadeEmptyFields": "", + "LookupQuery": "SELECT CAST(Id AS nvarchar(36)) AS [Key], Name AS [Name], ColorCode AS [Color] FROM Inv_T_AttributeValue WHERE IsDeleted = 'false' AND IsActive = 1 AND (AttributeId = @param0 OR @param0 IS NULL) ORDER BY SortOrder, Name" }, "ValidationRuleDto": [ { @@ -1165,7 +1165,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/ProductCategory.json b/configs/seeds/wizard/ProductCategory.json index a643c1a4..92c257b9 100644 --- a/configs/seeds/wizard/ProductCategory.json +++ b/configs/seeds/wizard/ProductCategory.json @@ -76,9 +76,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Inventory.ProductCategory.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Inv_T_ProductCategory\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -777,7 +777,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -915,7 +915,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1041,7 +1041,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1179,7 +1179,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, diff --git a/configs/seeds/wizard/ProductVariant.json b/configs/seeds/wizard/ProductVariant.json index 4b2e5179..c985378c 100644 --- a/configs/seeds/wizard/ProductVariant.json +++ b/configs/seeds/wizard/ProductVariant.json @@ -68,12 +68,6 @@ "En": "Variant Values", "Tr": "Varyant Değerleri" }, - { - "FieldName": "VariantKey", - "CaptionName": "App.Listform.ListformField.VariantKey", - "En": "Variant Key", - "Tr": "Varyant Anahtarı" - }, { "FieldName": "SalesPrice", "CaptionName": "App.Listform.ListformField.SalesPrice", @@ -789,7 +783,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -915,7 +909,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -1041,7 +1035,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1167,7 +1161,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1293,7 +1287,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, @@ -1326,17 +1320,17 @@ "JoinFieldName2": null, "BaseTableJoinFieldName2_OrValue": null }, - "FieldName": "VariantKey", - "CaptionName": "App.Listform.ListformField.VariantKey", + "FieldName": "SalesPrice", + "CaptionName": "App.Listform.ListformField.SalesPrice", "PlaceHolder": null, "ReadOnly": false, "Visible": true, "IsActive": true, "Width": 0, "ListOrderNo": 6, - "SourceDbType": 16, + "SourceDbType": 7, "CultureName": "en", - "DataType": "string", + "DataType": "number", "SortIndex": 0, "SortDirection": null, "AllowSearch": true, @@ -1419,13 +1413,13 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, - "EditOrderNo": null, - "EditGroupOrderNo": null, - "EditorType2": "dxTextBox", + "EditOrderNo": 4, + "EditGroupOrderNo": 1, + "EditorType2": "dxNumberBox", "ColSpan": 1, - "EditorOptions": null, + "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}", "GridBoxOptions": null, "TagBoxOptions": null, "ImageUploadOptions": null, @@ -1452,8 +1446,8 @@ "JoinFieldName2": null, "BaseTableJoinFieldName2_OrValue": null }, - "FieldName": "SalesPrice", - "CaptionName": "App.Listform.ListformField.SalesPrice", + "FieldName": "Cost", + "CaptionName": "App.Listform.ListformField.Cost", "PlaceHolder": null, "ReadOnly": false, "Visible": true, @@ -1545,133 +1539,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, - "Format": null, - "EditOrderNo": 4, - "EditGroupOrderNo": 1, - "EditorType2": "dxNumberBox", - "ColSpan": 1, - "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}", - "GridBoxOptions": null, - "TagBoxOptions": null, - "ImageUploadOptions": null, - "EditorScript": null, - "LastModificationTime": null, - "LastModifierId": null, - "CreationTime": "0001-01-01T00:00:00", - "CreatorId": null, - "Id": "00000000-0000-0000-0000-000000000000" - }, - { - "EditType": null, - "ListFormCode": "App.Inventory.ProductVariant", - "RoleId": null, - "UserId": null, - "ColumnJoinTableDto": { - "JoinType": null, - "TableName": null, - "JoinFieldName": null, - "BaseTableJoinFieldName": null, - "SelectFieldName": null, - "FieldNameAlias": null, - "JoinCondition2": "AND", - "JoinFieldName2": null, - "BaseTableJoinFieldName2_OrValue": null - }, - "FieldName": "Cost", - "CaptionName": "App.Listform.ListformField.Cost", - "PlaceHolder": null, - "ReadOnly": false, - "Visible": true, - "IsActive": true, - "Width": 0, - "ListOrderNo": 8, - "SourceDbType": 7, - "CultureName": "en", - "DataType": "number", - "SortIndex": 0, - "SortDirection": null, - "AllowSearch": true, - "AllowEditing": true, - "AllowAdding": true, - "ColumnCssClass": null, - "ColumnCssValue": null, - "BandName": null, - "ColumnFilterDto": { - "AllowFiltering": true, - "FilterOperations": null, - "SelectedFilterOperation": null, - "FilterValue": null - }, - "ColumnHeaderDto": { - "AllowHeaderFiltering": true, - "AllowSearch": false, - "DataSource": null, - "DataSourceString": "" - }, - "ColumnGroupingDto": { - "AllowGrouping": true, - "GroupIndex": null, - "AutoExpandGroup": false - }, - "ColumnCustomizationDto": { - "Fixed": false, - "FixedPosition": "left", - "AllowReordering": true - }, - "ColumnTotalSummaryDto": { - "SummaryType": null, - "ValueFormat": null, - "ShowInGroupFooter": false, - "AlignByColumn": false, - "DisplayFormat": null, - "ShowInColumn": null - }, - "ColumnGroupSummaryDto": { - "SummaryType": null, - "ValueFormat": null, - "ShowInGroupFooter": false, - "AlignByColumn": false, - "DisplayFormat": null, - "ShowInColumn": null - }, - "LookupDto": { - "DataSourceType": 0, - "ValueExpr": "Key", - "DisplayExpr": "Name", - "CascadeRelationField": null, - "CascadeFilterOperator": "=", - "CascadeParentFields": null, - "CascadeEmptyFields": null, - "LookupQuery": null - }, - "ValidationRuleDto": [], - "ColumnStylingDto": [], - "PermissionDto": { - "Deny": false, - "C": "App.Inventory.ProductVariant.Create", - "R": "App.Inventory.ProductVariant", - "U": "App.Inventory.ProductVariant.Update", - "E": true, - "I": true - }, - "CanRead": false, - "CanCreate": false, - "CanUpdate": false, - "CanExport": false, - "DefaultValue": null, - "DefaultValueType": 0, - "PivotSettingsDto": { - "IsPivot": true, - "Expanded": false, - "SortOrder": "asc", - "Area": "filter", - "Format": null, - "SummaryType": null, - "GroupInterval": "", - "WordWrapEnabled": false - }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 5, "EditGroupOrderNo": 1, @@ -1711,7 +1579,7 @@ "Visible": true, "IsActive": true, "Width": 0, - "ListOrderNo": 9, + "ListOrderNo": 8, "SourceDbType": 16, "CultureName": "en", "DataType": "string", @@ -1797,7 +1665,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 6, "EditGroupOrderNo": 1, @@ -1837,7 +1705,7 @@ "Visible": true, "IsActive": true, "Width": 0, - "ListOrderNo": 10, + "ListOrderNo": 9, "SourceDbType": 9, "CultureName": "en", "DataType": "string", @@ -1923,7 +1791,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 7, "EditGroupOrderNo": 1, @@ -1963,6 +1831,132 @@ "Visible": true, "IsActive": true, "Width": 0, + "ListOrderNo": 10, + "SourceDbType": 3, + "CultureName": "en", + "DataType": "boolean", + "SortIndex": 0, + "SortDirection": null, + "AllowSearch": true, + "AllowEditing": true, + "AllowAdding": true, + "ColumnCssClass": null, + "ColumnCssValue": null, + "BandName": null, + "ColumnFilterDto": { + "AllowFiltering": true, + "FilterOperations": null, + "SelectedFilterOperation": null, + "FilterValue": null + }, + "ColumnHeaderDto": { + "AllowHeaderFiltering": true, + "AllowSearch": false, + "DataSource": null, + "DataSourceString": "" + }, + "ColumnGroupingDto": { + "AllowGrouping": true, + "GroupIndex": null, + "AutoExpandGroup": false + }, + "ColumnCustomizationDto": { + "Fixed": false, + "FixedPosition": "left", + "AllowReordering": true + }, + "ColumnTotalSummaryDto": { + "SummaryType": null, + "ValueFormat": null, + "ShowInGroupFooter": false, + "AlignByColumn": false, + "DisplayFormat": null, + "ShowInColumn": null + }, + "ColumnGroupSummaryDto": { + "SummaryType": null, + "ValueFormat": null, + "ShowInGroupFooter": false, + "AlignByColumn": false, + "DisplayFormat": null, + "ShowInColumn": null + }, + "LookupDto": { + "DataSourceType": 0, + "ValueExpr": "Key", + "DisplayExpr": "Name", + "CascadeRelationField": null, + "CascadeFilterOperator": "=", + "CascadeParentFields": null, + "CascadeEmptyFields": null, + "LookupQuery": null + }, + "ValidationRuleDto": [], + "ColumnStylingDto": [], + "PermissionDto": { + "Deny": false, + "C": "App.Inventory.ProductVariant.Create", + "R": "App.Inventory.ProductVariant", + "U": "App.Inventory.ProductVariant.Update", + "E": true, + "I": true + }, + "CanRead": false, + "CanCreate": false, + "CanUpdate": false, + "CanExport": false, + "DefaultValue": null, + "DefaultValueType": 0, + "PivotSettingsDto": { + "IsPivot": true, + "Expanded": false, + "SortOrder": "asc", + "Area": "filter", + "Format": null, + "SummaryType": null, + "GroupInterval": "", + "WordWrapEnabled": false + }, + "Alignment": "left", + "Format": null, + "EditOrderNo": 8, + "EditGroupOrderNo": 1, + "EditorType2": "dxCheckBox", + "ColSpan": 1, + "EditorOptions": null, + "GridBoxOptions": null, + "TagBoxOptions": null, + "ImageUploadOptions": null, + "EditorScript": null, + "LastModificationTime": null, + "LastModifierId": null, + "CreationTime": "0001-01-01T00:00:00", + "CreatorId": null, + "Id": "00000000-0000-0000-0000-000000000000" + }, + { + "EditType": null, + "ListFormCode": "App.Inventory.ProductVariant", + "RoleId": null, + "UserId": null, + "ColumnJoinTableDto": { + "JoinType": null, + "TableName": null, + "JoinFieldName": null, + "BaseTableJoinFieldName": null, + "SelectFieldName": null, + "FieldNameAlias": null, + "JoinCondition2": "AND", + "JoinFieldName2": null, + "BaseTableJoinFieldName2_OrValue": null + }, + "FieldName": "IsDeleted", + "CaptionName": "App.Listform.ListformField.IsDeleted", + "PlaceHolder": null, + "ReadOnly": false, + "Visible": true, + "IsActive": true, + "Width": 0, "ListOrderNo": 11, "SourceDbType": 3, "CultureName": "en", @@ -2049,133 +2043,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, - "Format": null, - "EditOrderNo": 8, - "EditGroupOrderNo": 1, - "EditorType2": "dxCheckBox", - "ColSpan": 1, - "EditorOptions": null, - "GridBoxOptions": null, - "TagBoxOptions": null, - "ImageUploadOptions": null, - "EditorScript": null, - "LastModificationTime": null, - "LastModifierId": null, - "CreationTime": "0001-01-01T00:00:00", - "CreatorId": null, - "Id": "00000000-0000-0000-0000-000000000000" - }, - { - "EditType": null, - "ListFormCode": "App.Inventory.ProductVariant", - "RoleId": null, - "UserId": null, - "ColumnJoinTableDto": { - "JoinType": null, - "TableName": null, - "JoinFieldName": null, - "BaseTableJoinFieldName": null, - "SelectFieldName": null, - "FieldNameAlias": null, - "JoinCondition2": "AND", - "JoinFieldName2": null, - "BaseTableJoinFieldName2_OrValue": null - }, - "FieldName": "IsDeleted", - "CaptionName": "App.Listform.ListformField.IsDeleted", - "PlaceHolder": null, - "ReadOnly": false, - "Visible": true, - "IsActive": true, - "Width": 0, - "ListOrderNo": 12, - "SourceDbType": 3, - "CultureName": "en", - "DataType": "boolean", - "SortIndex": 0, - "SortDirection": null, - "AllowSearch": true, - "AllowEditing": true, - "AllowAdding": true, - "ColumnCssClass": null, - "ColumnCssValue": null, - "BandName": null, - "ColumnFilterDto": { - "AllowFiltering": true, - "FilterOperations": null, - "SelectedFilterOperation": null, - "FilterValue": null - }, - "ColumnHeaderDto": { - "AllowHeaderFiltering": true, - "AllowSearch": false, - "DataSource": null, - "DataSourceString": "" - }, - "ColumnGroupingDto": { - "AllowGrouping": true, - "GroupIndex": null, - "AutoExpandGroup": false - }, - "ColumnCustomizationDto": { - "Fixed": false, - "FixedPosition": "left", - "AllowReordering": true - }, - "ColumnTotalSummaryDto": { - "SummaryType": null, - "ValueFormat": null, - "ShowInGroupFooter": false, - "AlignByColumn": false, - "DisplayFormat": null, - "ShowInColumn": null - }, - "ColumnGroupSummaryDto": { - "SummaryType": null, - "ValueFormat": null, - "ShowInGroupFooter": false, - "AlignByColumn": false, - "DisplayFormat": null, - "ShowInColumn": null - }, - "LookupDto": { - "DataSourceType": 0, - "ValueExpr": "Key", - "DisplayExpr": "Name", - "CascadeRelationField": null, - "CascadeFilterOperator": "=", - "CascadeParentFields": null, - "CascadeEmptyFields": null, - "LookupQuery": null - }, - "ValidationRuleDto": [], - "ColumnStylingDto": [], - "PermissionDto": { - "Deny": false, - "C": "App.Inventory.ProductVariant.Create", - "R": "App.Inventory.ProductVariant", - "U": "App.Inventory.ProductVariant.Update", - "E": true, - "I": true - }, - "CanRead": false, - "CanCreate": false, - "CanUpdate": false, - "CanExport": false, - "DefaultValue": null, - "DefaultValueType": 0, - "PivotSettingsDto": { - "IsPivot": true, - "Expanded": false, - "SortOrder": "asc", - "Area": "filter", - "Format": null, - "SummaryType": null, - "GroupInterval": "", - "WordWrapEnabled": false - }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, diff --git a/configs/seeds/wizard/WorkCenter.json b/configs/seeds/wizard/WorkCenter.json index 464b2f69..87d13a1a 100644 --- a/configs/seeds/wizard/WorkCenter.json +++ b/configs/seeds/wizard/WorkCenter.json @@ -94,9 +94,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Inventory.WorkCenter.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Inv_T_WorkCenter\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, diff --git a/configs/seeds/wizard/WorkCenterTag.json b/configs/seeds/wizard/WorkCenterTag.json index 0c8b9473..d3fff6e4 100644 --- a/configs/seeds/wizard/WorkCenterTag.json +++ b/configs/seeds/wizard/WorkCenterTag.json @@ -76,9 +76,9 @@ "SelectCommandType": 1, "SelectFieldsDefaultValueDto": [], "SeedFilePath": "data/App.Inventory.WorkCenterTag.json", - "SeedSyncInsert": true, - "SeedSyncUpdate": true, - "SeedSyncDelete": true, + "SeedSyncInsert": false, + "SeedSyncUpdate": false, + "SeedSyncDelete": false, "DeleteCommand": "UPDATE \"Inv_T_WorkCenterTag\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id", "DeleteBeforeCommand": null, "DeleteAfterCommand": null, @@ -777,7 +777,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": null, "EditGroupOrderNo": null, @@ -915,7 +915,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 1, "EditGroupOrderNo": 1, @@ -1041,7 +1041,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 2, "EditGroupOrderNo": 1, @@ -1179,7 +1179,7 @@ "GroupInterval": "", "WordWrapEnabled": false }, - "Alignment": null, + "Alignment": "left", "Format": null, "EditOrderNo": 3, "EditGroupOrderNo": 1, diff --git a/ui/src/components/scriptBuilder/ScriptBuilderDialog.tsx b/ui/src/components/scriptBuilder/ScriptBuilderDialog.tsx index 8e16ba9b..cd97cbd6 100644 --- a/ui/src/components/scriptBuilder/ScriptBuilderDialog.tsx +++ b/ui/src/components/scriptBuilder/ScriptBuilderDialog.tsx @@ -79,8 +79,12 @@ function ScriptBuilderDialog({ const cancelText = cancelLabel ?? translate('::App.Platform.Cancel') const [rules, setRules] = useState([]) const [code, setCode] = useState('') + const [syntaxError, setSyntaxError] = useState('') // Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür. const [view, setView] = useState<'code' | 'wizard'>('code') + + // Önceki kaydetme denemesinin hatası, metin değişince ya da diyalog yeniden açılınca geçersizdir. + useEffect(() => setSyntaxError(''), [code, view, isOpen]) const [openRules, setOpenRules] = useState>({}) const [paletteOpen, setPaletteOpen] = useState(true) const [helpOpen, setHelpOpen] = useState(true) @@ -102,6 +106,13 @@ function ScriptBuilderDialog({ const generatedScript = useMemo(() => dialect.buildScript(rules), [dialect, rules]) const completeRuleCount = rules.filter((rule) => dialect.isRuleComplete(rule)).length const codeDiverged = code.trim() !== '' && code.trim() !== generatedScript.trim() + // Kural başlığı taşıyan ama elle değiştirilmiş kod: başlık artık gövdeyi anlatmaz. Kalırsa + // dialog ve sunucu (`EditorScript.Rebuild`) gövdeyi başlıktan yeniden üretip elle yapılan + // düzeltmeyi geri alır. + const staleHeader = useMemo(() => { + const parsed = dialect.parseScript(code) + return parsed.hasHeader && !parsed.inSync + }, [dialect, code]) useEffect(() => { if (!isOpen) return @@ -284,9 +295,7 @@ function ScriptBuilderDialog({ menuPortalTarget={document.body} options={options.map((option) => ({ value: option, label: option }))} value={ - options.includes(selectedValue) - ? { value: selectedValue, label: selectedValue } - : null + options.includes(selectedValue) ? { value: selectedValue, label: selectedValue } : null } onChange={(option) => onChange(option?.value ?? '')} /> @@ -341,6 +350,10 @@ function ScriptBuilderDialog({ } const currentValue = rule.params[param.key] ?? '' + const ruleChoices = + param.type === 'text' ? dialect.getParamChoices?.(rule, param.key) : undefined + const choices = ruleChoices ?? param.choices + const inputType = ruleChoices ? 'select' : param.type return ( )} @@ -836,6 +845,13 @@ function ScriptBuilderDialog({ )} + {view === 'code' && staleHeader && ( +
+ + {translate('::App.ScriptBuilder.StaleHeader')} +
+ )} + {view === 'code' ? (
@@ -966,6 +982,11 @@ function ScriptBuilderDialog({ + {syntaxError && ( +

+ {translate('::App.ScriptBuilder.SyntaxError', { message: syntaxError })} +

+ )} @@ -975,7 +996,18 @@ function ScriptBuilderDialog({ variant="solid" icon={} onClick={() => { - onApply(view === 'wizard' ? generatedScript : code) + const script = + view === 'wizard' + ? generatedScript + : staleHeader + ? code.trim().split('\n').slice(1).join('\n').trim() + : code + // Runtime script'i derleyemezse tamamı (diğer kurallar dahil) sessizce çalışmaz; + // bozuk metin kaydedilmeden önce yakalanır. + const error = findSyntaxError(script) + setSyntaxError(error) + if (error) return + onApply(script) onClose() }} > @@ -986,4 +1018,18 @@ function ScriptBuilderDialog({ ) } +const AsyncFunction = Object.getPrototypeOf(async function noop() {}).constructor as new ( + ...args: string[] +) => unknown + +/** Script runtime'ın derleyebileceği bir gövde mi; değilse derleyicinin mesajı. */ +const findSyntaxError = (script: string) => { + try { + new AsyncFunction(script) + return '' + } catch (error) { + return error instanceof SyntaxError ? error.message : '' + } +} + export default ScriptBuilderDialog diff --git a/ui/src/components/scriptBuilder/types.ts b/ui/src/components/scriptBuilder/types.ts index 9a522e95..8c1802ba 100644 --- a/ui/src/components/scriptBuilder/types.ts +++ b/ui/src/components/scriptBuilder/types.ts @@ -151,6 +151,11 @@ export interface ScriptDialect { getTrigger?: (rule: ScriptRule) => string /** Kural kartında gösterilecek uyarı; örneğin eksik bağlam. */ getRuleWarning?: (rule: ScriptRule) => string + /** + * Serbest metin parametresinin kurala göre sabit seçenekleri (mantıksal alana yazılan değer için + * true/false). Seçenek dönerse dialog metin kutusu yerine seçici gösterir. + */ + getParamChoices?: (rule: ScriptRule, paramKey: string) => ScriptRecipeParam['choices'] } /** Kuralın bütün koşulları; ilki her zaman `condition`. */ diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index 8e2f2f29..a6473920 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -77,6 +77,8 @@ export interface ListFormWizardDto { * Wizard adımlarında sorulmaz (MCP ile demo veri taşıyan ekranlar açar), düzenlemede korunur. */ seedData: boolean + /** Ekrandan yapılan kayıtlar veri dosyasına yansısın mı; varsayılan kapalı, düzenlemede korunur. */ + seedSync: boolean defaultLayout: ListViewLayoutType grid: boolean diff --git a/ui/src/utils/editorScriptRuntime.ts b/ui/src/utils/editorScriptRuntime.ts index 2d7a2dfe..a4f6b76e 100644 --- a/ui/src/utils/editorScriptRuntime.ts +++ b/ui/src/utils/editorScriptRuntime.ts @@ -13,6 +13,9 @@ * yeniden derlenir. */ +import { createElement } from 'react' +import Notification from '@/components/ui/Notification' +import toast from '@/components/ui/toast' import apiService from '@/services/api.service' import { getLocalization } from '@/services/localization.service' import { store } from '@/store' @@ -69,9 +72,36 @@ const HELPER_PARAMS = [ * ------------------------------------------------------------------ */ /** Script runtime React ağacının dışındadır; dil metni store'dan okunur. */ -const translateRuntime = (key: string) => { +const translateRuntime = (key: string, params?: Record) => { const { texts, config } = store.getState().abpConfig - return getLocalization(texts ?? {}, config?.localization?.defaultResourceName, key) + return getLocalization(texts ?? {}, config?.localization?.defaultResourceName, key, params) +} + +/** Form her yeniden çizildiğinde aynı script tekrar çalışır; aynı hata kısa sürede bir kez gösterilir. */ +const REPEATED_ERROR_WINDOW_MS = 5000 +const lastReportedErrors = new Map() + +/** + * Script hatasını kullanıcıya gösterir. Konsola düşen hata kullanıcıya görünmez; bozuk bir kural + * sessizce çalışmadığında ekran "bozuk" görünür ama nedeni bilinmez. + */ +export const reportEditorScriptError = (field: string, error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + console.error('Editor script error:', field, error) + + const key = `${field}|${message}` + const now = Date.now() + if (now - (lastReportedErrors.get(key) ?? 0) < REPEATED_ERROR_WINDOW_MS) return + lastReportedErrors.set(key, now) + + toast.push( + createElement( + Notification, + { type: 'danger', duration: 6000 }, + translateRuntime('::App.EditorScript.Error', { field, message }), + ), + { placement: 'top-end' }, + ) } /** Alan adını büyük/küçük harf duyarsız arar; SQL kolonları tutarsız gelebiliyor. */ @@ -98,8 +128,7 @@ const toDate = (input: any): Date | null => { return Number.isNaN(parsed.getTime()) ? null : parsed } -const startOfDayUtc = (date: Date) => - Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) +const startOfDayUtc = (date: Date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) /* ------------------------------------------------------------------ * * Yardımcı API üretimi @@ -156,9 +185,15 @@ const buildHelpers = ({ queueMicrotask(flush) } - const writeOne = (name: string, next: any) => { + const writeOne = (name: string, input: any) => { if (!name) return const key = resolveKey(data, name) ?? name + // Mantıksal alana şablondan gelen 'false'/'0' metni dolu sayılırdı; mevcut değer + // mantıksalsa yazılan değer de mantıksala çevrilir. + const next = + typeof data[key] === 'boolean' && /^(true|false|1|0)$/i.test(String(input)) + ? /^(true|1)$/i.test(String(input)) + : input // Değer değişmediyse yazmıyoruz. Form açılışında (contentReady) çalışan // script'ler setFormData'yı tetikleyip yeniden contentReady doğurabildiği // için bu koruma olmadan sonsuz döngü oluşur. @@ -191,8 +226,7 @@ const buildHelpers = ({ const clear = (...names: string[]) => set(Object.fromEntries(names.map((name) => [name, null]))) - const readOnly = (name: string, flag: boolean = true) => - runtimeSetEditorReadOnly?.(name, !!flag) + const readOnly = (name: string, flag: boolean = true) => runtimeSetEditorReadOnly?.(name, !!flag) /** Lookup/GridBox ile seçilen kaydın bir kolonunu form alanına taşır. */ const copy = (sourcePath: string, targetName?: string) => { @@ -230,13 +264,14 @@ const buildHelpers = ({ const tpl = (text: string) => String(text ?? '') .replace(/\{value\}/g, () => String(e?.value ?? '')) - .replace(/\{selected\.([^}]+)\}/g, (_, key: string) => String(pickPath(selectedItem, key) ?? '')) + .replace(/\{selected\.([^}]+)\}/g, (_, key: string) => + String(pickPath(selectedItem, key) ?? ''), + ) .replace(/\{([^}]+)\}/g, (_, key: string) => String(get(key) ?? '')) /** Geçerli alanı değişiklik öncesindeki değerine döndürür. */ const revert = () => { - const previous = - e?.previousValue ?? pickPath(formData, field) ?? null + const previous = e?.previousValue ?? pickPath(formData, field) ?? null writeOne(field, previous) const activeEditor = e?.component?.getEditor?.(field) if (activeEditor?.option) activeEditor.option('value', previous) @@ -405,7 +440,15 @@ const compileScript = (script: string): CompiledScript => { export const executeEditorScript = (script: string, context: EditorScriptRuntimeContext) => { const { formData, e, editor, runtimeSetEditorReadOnly, setFormData } = context - const compiled = compileScript(script) + const scriptField = String(editor?.dataField ?? e?.dataField ?? '') + + let compiled: CompiledScript + try { + compiled = compileScript(script) + } catch (error) { + reportEditorScriptError(scriptField, error) + return Promise.resolve() + } const { args, flush } = buildHelpers(context) const baseArgs = [formData, e, editor, runtimeSetEditorReadOnly, setFormData] @@ -415,8 +458,6 @@ export const executeEditorScript = (script: string, context: EditorScriptRuntime flush() return result - .catch((error: unknown) => { - console.error('Editor script error:', error) - }) + .catch((error: unknown) => reportEditorScriptError(scriptField, error)) .finally(flush) } diff --git a/ui/src/views/admin/listForm/edit/form-fields/FormFieldTabOptions.tsx b/ui/src/views/admin/listForm/edit/form-fields/FormFieldTabOptions.tsx index b113288c..2db02476 100644 --- a/ui/src/views/admin/listForm/edit/form-fields/FormFieldTabOptions.tsx +++ b/ui/src/views/admin/listForm/edit/form-fields/FormFieldTabOptions.tsx @@ -6,7 +6,7 @@ import { Field, FieldProps, Form, Formik } from 'formik' import { bool, object, string } from 'yup' import { columnEditorTypeListOptions, sortDirectionOptions } from '../options' import { FormFieldEditProps } from './FormFields' -import { EditingFormDto, SortDirectionEnum } from '@/proxy/form/models' +import { DbTypeEnum, EditingFormDto, SortDirectionEnum } from '@/proxy/form/models' import { FaCode } from 'react-icons/fa' import { useState } from 'react' import EditorScriptBuilderDialog from '../json-row-operations/EditorScriptBuilderDialog' @@ -52,6 +52,16 @@ function FormFieldTabOptions({ value: field.fieldName, label: field.captionName || field.fieldName, })) + const booleanFields = fields + .filter( + (field) => + field.fieldName && + (field.sourceDbType === DbTypeEnum.Boolean || + field.dataType === 'boolean' || + field.editorType2 === 'dxCheckBox' || + field.editorType2 === 'dxSwitch'), + ) + .map((field) => field.fieldName!) return ( setIsEditorScriptDialogOpen(false)} onApply={(val) => setFieldValue('editorScript', val)} /> diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx index 63c899ca..dbba3bc3 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx +++ b/ui/src/views/admin/listForm/edit/json-row-operations/EditorScriptBuilderDialog.tsx @@ -9,6 +9,7 @@ type EditorScriptBuilderDialogProps = { value?: string currentField?: string fields: SelectBoxOption[] + booleanFields?: string[] onClose: () => void onApply: (value: string) => void } @@ -22,6 +23,7 @@ function EditorScriptBuilderDialog({ value, currentField, fields, + booleanFields, onClose, onApply, }: EditorScriptBuilderDialogProps) { @@ -30,9 +32,16 @@ function EditorScriptBuilderDialog({ // Çağıran her render'da yeni bir dizi verebilir; lehçe alan adlarına göre // sabitlenmezse editör yazarken sıfırlanırdı. const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|') + const booleanKey = (booleanFields ?? []).join('|') const dialect = useMemo( - () => createFormScriptDialect({ currentField, fields: fieldKey.split('|'), translate }), - [currentField, fieldKey, translate], + () => + createFormScriptDialect({ + currentField, + fields: fieldKey.split('|'), + booleanFields: booleanKey.split('|'), + translate, + }), + [currentField, fieldKey, booleanKey, translate], ) return ( diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/formScriptDialect.ts b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/formScriptDialect.ts index 36da1a2a..51be388f 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/formScriptDialect.ts +++ b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/formScriptDialect.ts @@ -137,6 +137,8 @@ const snippets: ScriptSnippet[] = [ export type FormScriptDialectContext = { /** Form kolonları. */ fields: string[] + /** Mantıksal (onay kutusu / anahtar) alanlar; değer yazan kurallarda true/false seçtirilir. */ + booleanFields?: string[] /** Script'in bağlı olduğu alan; başlıkta gösterilir. */ currentField?: string /** `useLocalization` hook'undan gelen çeviri fonksiyonu. */ @@ -150,6 +152,11 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri ) const asFormRule = (rule: ScriptRule) => rule as unknown as FormScriptRule + const booleanFields = new Set((context.booleanFields ?? []).map((field) => field.toLowerCase())) + const booleanChoices = [ + { value: 'true', label: t('::App.Platform.Yes') }, + { value: 'false', label: t('::App.Platform.No') }, + ] // Tarif listesi hem "Kural ekle" seçicisinde hem de kural kartında kullanılıyor. // Tek bir çevrilmiş liste üretip ikisini de buradan besliyoruz; aksi halde kart @@ -241,11 +248,16 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri // Kosulsuz kuralda "Her zaman ->" oneki bilgi tasimiyor, yalnizca gurultu. if (!conditions.length) return action const joiner = - rule.join === 'or' - ? ` ${t('::App.Platform.Or')} ` - : ` ${t('::App.Platform.And')} ` + rule.join === 'or' ? ` ${t('::App.Platform.Or')} ` : ` ${t('::App.Platform.And')} ` return `${conditions.join(joiner)} → ${action}` }, getTrigger: (rule) => effectiveTrigger(asFormRule(rule)), + // Onay kutusuna metin ('0', 'Hayır') yazmak dolu sayılırdı; değer mantıksal sabit olarak seçilir. + getParamChoices: (rule, paramKey) => + rule.recipe === 'setValue' && + paramKey === 'text' && + booleanFields.has(String(rule.params.target ?? '').toLowerCase()) + ? booleanChoices + : undefined, } } diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts index e32e768e..066b286a 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts +++ b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts @@ -157,6 +157,15 @@ export type TranslateFn = (key: string, params?: Record const q = (input: string) => JSON.stringify(input ?? '') +/** + * `true`/`false` mantıksal sabit olarak yazılır; metin (`tpl('false')`) onay kutusunu dolu sayardı. + * Diğer değerler şablondur. `EditorScriptRule.ValueLiteral` (C#) ile aynı. + */ +const valueLiteral = (text: string) => { + const trimmed = String(text ?? '').trim() + return /^(true|false)$/i.test(trimmed) ? trimmed.toLowerCase() : `tpl(${q(text)})` +} + const label = (input: string, fallback: string) => input || fallback const isNumericLiteral = (input: string) => /^-?\d+(\.\d+)?$/.test(String(input).trim()) @@ -508,7 +517,7 @@ export const recipes: Recipe[] = [ help: 'App.ScriptBuilderText.Help', }, ], - build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`, + build: ({ params }) => `set(${q(params.target)}, ${valueLiteral(params.text)})`, describe: ({ params }, t) => t('::App.ScriptBuilderDescribe.SetValue', { target: label(params.target, t('::App.Listform.ListformField.Target')), diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index 42fbd7a5..b42375a2 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -73,6 +73,7 @@ const initialValues: ListFormWizardDto = { confirmDelete: true, allowDetail: false, seedData: false, + seedSync: false, defaultLayout: 'grid', grid: true, card: true, @@ -715,6 +716,7 @@ const Wizard = () => { confirmDelete: w.confirmDelete ?? true, allowDetail: w.allowDetail ?? false, seedData: w.seedData ?? false, + seedSync: w.seedSync ?? false, defaultLayout: w.defaultLayout ?? 'grid', grid: w.grid ?? initialValues.grid, card: w.card ?? initialValues.card, diff --git a/ui/src/views/list/shared/cascade.ts b/ui/src/views/list/shared/cascade.ts index 5bdcfdcc..a8026a10 100644 --- a/ui/src/views/list/shared/cascade.ts +++ b/ui/src/views/list/shared/cascade.ts @@ -3,7 +3,7 @@ * Grid, Tree ve form sayfası aynı kuralları paylaşır. */ import type { ColumnFormatDto, GridDto } from '@/proxy/form/models' -import { getActiveEditingForm } from './editingForm' +import { getActiveEditingForm, getEditingRow } from './editingForm' type CascadeInfo = { parentFields: string[] @@ -99,16 +99,6 @@ export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) => maps.parentToChildrenMap.has(fieldName) || maps.clearFieldsMap.has(fieldName) -const getEditingRow = (grid: any) => { - const rowKey = grid.option('editing.editRowKey') - return { - rowIndex: grid.getRowIndexByKey(rowKey) as number, - rowData: grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data as - | Record - | undefined, - } -} - /** Parent değiştikten sonra etkilenen çocuk editörlerin kilidini yeniler. */ const updateChildDisabledStates = ( grid: any, diff --git a/ui/src/views/list/shared/editingForm.ts b/ui/src/views/list/shared/editingForm.ts index fae0e61e..7300347a 100644 --- a/ui/src/views/list/shared/editingForm.ts +++ b/ui/src/views/list/shared/editingForm.ts @@ -222,11 +222,25 @@ export const setFormEditorReadOnly = (form: any, field: string, readOnly: boolea return true } +/** DataGrid/TreeList popup düzenleme formu; düzenleme denetleyicisinde yaşar. */ export const getActiveEditingForm = (component: any) => { - const editForm = component?.getView?.('editingView')?._editForm + const editForm = component?.getController?.('editing')?._editForm return editForm?.getEditor || editForm?.itemOption ? editForm : undefined } +/** + * Düzenlenen satırın güncel verisi (`cellValue` ile yazılanlar dahil). Yeni satırın anahtarı + * nesnedir; `row.key === rowKey` eşleşmez, satır grid'in değerle eşleyen indeksinden okunur. + */ +export const getEditingRow = (grid: any) => { + const rowKey = grid?.option?.('editing.editRowKey') + const rowIndex: number = + rowKey !== undefined && rowKey !== null ? (grid?.getRowIndexByKey?.(rowKey) ?? -1) : -1 + const rowData: Record | undefined = + rowIndex >= 0 ? grid?.getVisibleRows?.()?.[rowIndex]?.data : undefined + return { rowKey, rowIndex, rowData } +} + /** * Form ilk açıldığında da çalışması gereken script'leri ayıklar. * @@ -242,11 +256,11 @@ export const getActiveEditingForm = (component: any) => { export const shouldRunEditorScriptOnContentReady = (script?: string) => Boolean( script && - (script.includes('@runOnOpen') || - /\breadOnly\s*\(/.test(script) || - /\bisReady\b/.test(script) || - script.includes('setEditorReadOnly') || - script.includes('runtimeSetEditorReadOnly')), + (script.includes('@runOnOpen') || + /\breadOnly\s*\(/.test(script) || + /\bisReady\b/.test(script) || + script.includes('setEditorReadOnly') || + script.includes('runtimeSetEditorReadOnly')), ) /* ------------------------------------------------------------------ * @@ -537,7 +551,11 @@ export const createFormItemMapper = ({ defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder) } - if (typeof colFormat.defaultValue === 'string' && colFormat.defaultValue === '@AUTONUMBER' && mode === 'new') { + if ( + typeof colFormat.defaultValue === 'string' && + colFormat.defaultValue === '@AUTONUMBER' && + mode === 'new' + ) { defaultEditorOptions.value = autoNumber() } diff --git a/ui/src/views/list/shared/editingFormScripts.ts b/ui/src/views/list/shared/editingFormScripts.ts index 636592c2..ae3b591d 100644 --- a/ui/src/views/list/shared/editingFormScripts.ts +++ b/ui/src/views/list/shared/editingFormScripts.ts @@ -5,7 +5,7 @@ * birebir tekrar ediyordu; buradan tek noktadan sağlanır. */ import type { MutableRefObject } from 'react' -import { executeEditorScript } from '@/utils/editorScriptRuntime' +import { executeEditorScript, reportEditorScriptError } from '@/utils/editorScriptRuntime' import { attachFieldValueBroadcast } from '@/utils/phoneFormat' import type { GridDto } from '@/proxy/form/models' import type { RowMode } from '../../form/types' @@ -19,6 +19,7 @@ import { } from './cascade' import { getActiveEditingForm, + getEditingRow, getValueByField, resolveEditorButtons, setFormEditorReadOnly, @@ -34,14 +35,6 @@ type EditingFormHandlerParams = { editingFormInstanceRef: MutableRefObject } -const getEditingRowIndex = (grid: any) => { - const rowKey = grid?.option?.('editing.editRowKey') - return { - rowKey, - rowIndex: rowKey !== undefined ? (grid?.getRowIndexByKey?.(rowKey) ?? -1) : -1, - } -} - /** * Grid hücrelerine script tarafından yazılan değerleri uygular. * CardView gibi `cellValue` API'si olmayan bileşenlerde sessizce atlanır; @@ -52,6 +45,99 @@ const createGridCellWriter = (grid: any, rowIndex: number) => (newData: Record grid.cellValue(rowIndex, field, newData[field])) } +/** + * Zincirde bir alanın aynı değerle ikinci kez tetiklenmesi döngüdür (A → B → A). Derinlik sınırı + * yoktur; değeri her turda değişen döngüyü (sayaç gibi) bu emniyet sınırı yakalar. + */ +const MAX_CHAIN_STEPS = 1000 + +const chainStep = (field: string, value: unknown) => { + try { + return `${field.toLowerCase()}=${JSON.stringify(value ?? null)}` + } catch { + return `${field.toLowerCase()}=${String(value)}` + } +} + +type ChainedScriptParams = { + gridDto: GridDto | undefined + grid: any + form: any + before: Record + after: Record + sourceField?: string + /** Zincirin bu ana kadar çalıştırdığı alan=değer adımları; ilk çağrıda verilmez. */ + visited?: Set +} + +/** + * Script'in `set` ile yazdığı alanların kendi script'lerini çalıştırır; onların yazdıkları da aynı + * şekilde devam eder. Grid'e `cellValue` ile yazılan değer editörün değişiklik olayını tetiklemez; + * zincir olmadan "TrackInventory false olunca TrackingMethod salt okunur" gibi bağımlı kurallar + * kullanıcı alanı elle değiştirene kadar çalışmazdı. Form sayfası (`updateData`) olayı zaten + * tetiklediği için yalnızca Grid/Tree'de gerekir. + */ +const runChainedScripts = ({ + gridDto, + grid, + form, + before, + after, + sourceField, + visited = new Set(sourceField ? [chainStep(sourceField, after[sourceField])] : []), +}: ChainedScriptParams) => { + if (!gridDto) return + + const source = String(sourceField ?? '').toLowerCase() + const fields = getEditingFormFields(gridDto) + const changed = Object.keys(after).filter( + (key) => key.toLowerCase() !== source && !Object.is(before?.[key], after[key]), + ) + + changed.forEach((key) => { + const format = fields.find( + (field) => String(field.fieldName ?? '').toLowerCase() === key.toLowerCase(), + ) + if (!format?.editorScript || !format.fieldName) return + + const step = chainStep(format.fieldName, after[key]) + if (visited.has(step) || visited.size >= MAX_CHAIN_STEPS) { + reportEditorScriptError( + format.fieldName, + new Error(`${[...visited].map((item) => item.split('=')[0]).join(' → ')} → ${key}`), + ) + return + } + visited.add(step) + + const writeToGrid = createGridCellWriter(grid, getEditingRow(grid).rowIndex) + executeEditorScript(format.editorScript, { + formData: { ...after }, + e: { + component: form, + dataField: format.fieldName, + value: after[key], + scriptEvent: 'valueChanged', + }, + editor: { dataField: format.fieldName, component: grid }, + runtimeSetEditorReadOnly: (field: string, flag: boolean) => + setFormEditorReadOnly(form, field, flag), + setFormData: (next: Record) => { + writeToGrid(next) + runChainedScripts({ + gridDto, + grid, + form, + before: after, + after: next, + sourceField: format.fieldName, + visited, + }) + }, + }) + }) +} + export const createEditingFormHandlers = ({ getGridDto, getGridInstance, @@ -89,9 +175,13 @@ export const createEditingFormHandlers = ({ return values }, {}) + // Popup formu her `cellValue` yazımında yeniden çizilir ve bu olay yeniden gelir; formun + // `formData`'sı açılıştaki nesnedir, güncel değerler grid'in düzenlenen satırındadır. Eski + // değerle çalışan script kendi verdiği readOnly kararını geri alırdı. const formData = { ...editingFormDataRef.current, ...(form?.option?.('formData') || {}), + ...(getEditingRow(grid).rowData || {}), ...editorValues, } editingFormDataRef.current = { ...formData } @@ -99,7 +189,7 @@ export const createEditingFormHandlers = ({ const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) => setFormEditorReadOnly(form, field, readOnly) - const { rowIndex } = getEditingRowIndex(grid) + const { rowIndex } = getEditingRow(grid) const writeToGrid = createGridCellWriter(grid, rowIndex) /** @@ -152,7 +242,7 @@ export const createEditingFormHandlers = ({ setFormData: applyFormData, }) } catch (err) { - console.error('Script exec error on contentReady', field.fieldName, err) + reportEditorScriptError(field.fieldName!, err) } }) } finally { @@ -177,7 +267,7 @@ export const createEditingFormHandlers = ({ try { const grid = getGridInstance() - const { rowIndex } = getEditingRowIndex(grid) + const { rowIndex } = getEditingRow(grid) const formData = { ...(e.component?.option?.('formData') || {}), [e.dataField]: e.value, @@ -196,10 +286,18 @@ export const createEditingFormHandlers = ({ e.component?.option?.('formData', newData) editingFormDataRef.current = { ...newData } writeToGrid(newData) + runChainedScripts({ + gridDto, + grid, + form: e.component, + before: formData, + after: newData, + sourceField: e.dataField, + }) }, }) } catch (err) { - console.error('Script exec error', formField.fieldName, err) + reportEditorScriptError(formField.fieldName!, err) } } @@ -208,6 +306,7 @@ export const createEditingFormHandlers = ({ type EditorValueChangedScriptParams = { editor: any + gridDto: GridDto editorScript: string fieldName?: string editingFormInstanceRef: MutableRefObject @@ -218,6 +317,7 @@ type EditorValueChangedScriptParams = { */ const attachEditorValueChangedScript = ({ editor, + gridDto, editorScript, fieldName, editingFormInstanceRef, @@ -230,26 +330,31 @@ const attachEditorValueChangedScript = ({ try { const grid = editor.component - const { rowKey, rowIndex } = getEditingRowIndex(grid) - const formData = { - ...(grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}), - [editorDataField]: e.value, - } + const { rowIndex, rowData } = getEditingRow(grid) + const formData = { ...(rowData || {}), [editorDataField]: e.value } + const form = editingFormInstanceRef.current ?? getActiveEditingForm(grid) + const writeToGrid = createGridCellWriter(grid, rowIndex) executeEditorScript(editorScript, { formData, e: { ...e, scriptEvent: 'valueChanged' }, editor, runtimeSetEditorReadOnly: (field: string, readOnly: boolean) => - setFormEditorReadOnly( - editingFormInstanceRef.current ?? getActiveEditingForm(grid), - field, - readOnly, - ), - setFormData: createGridCellWriter(grid, rowIndex), + setFormEditorReadOnly(form, field, readOnly), + setFormData: (newData: Record) => { + writeToGrid(newData) + runChainedScripts({ + gridDto, + grid, + form, + before: formData, + after: newData, + sourceField: editorDataField, + }) + }, }) } catch (err) { - console.error('Script exec error', fieldName, err) + reportEditorScriptError(fieldName ?? '', err) } } } @@ -263,9 +368,8 @@ const attachEditorButtonContext = (editor: any, mode: RowMode) => { editor.editorOptions.buttons = withButtonContext(editor.editorOptions.buttons, () => { const grid = editor.component - const { rowKey, rowIndex } = getEditingRowIndex(grid) - const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {} - return { formData, fieldName: editor.dataField, mode, rowKey, rowIndex } + const { rowKey, rowIndex, rowData } = getEditingRow(grid) + return { formData: rowData || {}, fieldName: editor.dataField, mode, rowKey, rowIndex } }) } @@ -333,6 +437,7 @@ export const prepareEditingEditor = ({ if (columnFormat?.editOrderNo != null && columnFormat.editorScript) { attachEditorValueChangedScript({ editor, + gridDto, editorScript: columnFormat.editorScript, fieldName: columnFormat.fieldName, editingFormInstanceRef, @@ -395,19 +500,31 @@ export const createCustomEditorCellAdapter = if (columnFormat?.editOrderNo == null || !columnFormat.editorScript) return - const { rowIndex } = getEditingRowIndex(grid) + const { rowIndex } = getEditingRow(grid) const activeForm = editingFormInstanceRef.current ?? getActiveEditingForm(grid) + const formData = { ...rowData, [fieldName]: value } + const writeToGrid = createGridCellWriter(grid, rowIndex) try { executeEditorScript(columnFormat.editorScript, { - formData: { ...rowData, [fieldName]: value }, + formData, e: { component: activeForm, dataField: fieldName, value, scriptEvent: 'valueChanged' }, editor: { dataField: fieldName, component: grid }, runtimeSetEditorReadOnly: (field: string, flag: boolean) => setFormEditorReadOnly(activeForm, field, flag), - setFormData: createGridCellWriter(grid, rowIndex), + setFormData: (newData: Record) => { + writeToGrid(newData) + runChainedScripts({ + gridDto, + grid, + form: activeForm, + before: formData, + after: newData, + sourceField: fieldName, + }) + }, }) } catch (err) { - console.error('Script exec error', fieldName, err) + reportEditorScriptError(fieldName ?? '', err) } }