From 0bc44e726c073d6c279b556371ca58abd41bd5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:52:33 +0300 Subject: [PATCH] =?UTF-8?q?WizardFileManager=20g=C3=BCncellemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lowcode-reference.instructions.md | 10 +- .github/instructions/lowcode.instructions.md | 132 +++++++-- README.md | 4 +- .../Wizard/IListFormWizardAppService.cs | 6 + .../ListForms/Wizard/ListFormWizardDto.cs | 4 +- .../ListForms/Wizard/WizardSeedFileDto.cs | 25 ++ .../ListForms/ListFormWizardAppService.cs | 250 +++++++++++++++++- .../ListForms/WizardRecordCleaner.cs | 18 +- .../ListForms/WizardTableCleaner.cs | 8 +- .../WizardConsts.cs | 6 + .../Queries/SelectQueryManager.cs | 24 +- claude.md | 9 +- .../ce4269ad-8d08-487a-9e36-c002dd482a3c.json | 10 +- .../host/data/App.Languages.LanguageText.json | 98 +++++++ ui/src/proxy/admin/wizard/models.ts | 13 + ui/src/services/wizard.service.ts | 18 ++ .../listForm/wizard/WizardFileManager.tsx | 166 ++++++++++-- ui/src/views/list/CardView.tsx | 2 +- ui/src/views/list/GanttView.tsx | 2 +- ui/src/views/list/Grid.tsx | 2 +- ui/src/views/list/Pivot.tsx | 4 +- ui/src/views/list/SchedulerView.tsx | 2 +- ui/src/views/list/Tree.tsx | 2 +- ui/src/views/list/shared/hooks.ts | 16 +- 24 files changed, 757 insertions(+), 74 deletions(-) diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 7a5d1a1a..da63ab91 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -103,8 +103,8 @@ ve çoğu isteğin cevabı aslında oradadır: | `DataSourceCode` | `"Default"` | Hangi bağlantıdan okunacağı (`Sas_H_DataSource`) | | `SelectCommandType` | `1` Table | `1` Table · `2` View · `3` TableValuedFunction · `4` Query · `5` StoredProcedure | | `SelectCommand` | — | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni | -| `TableName` | `SelectCommand` | Üretilen `SELECT`'te kolonların ve join'lerin önüne yazılan **alias**. Boşsa `SelectCommand` kullanılır; `SelectCommand` bir sorgu metni ya da fonksiyon çağrısıysa buraya kısa bir ad yaz | -| `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı | +| `TableName` | `SelectCommand` | Üretilen `SELECT`'te kolonların ve join'lerin önüne yazılan **alias**. Boşsa `SelectCommand` kullanılır. `4` Query'de **zorunludur**: sorgu `FROM (…) AS "{TableName}"` biçiminde türetilmiş tablo olarak sarılır ve boş bırakılırsa sorgu metni alias olur (SQL hatası). Wizard/MCP `deploy_wizard` Query ekranına `"Q"` (`WizardConsts.QuerySourceAlias`) yazar; fonksiyon çağrısında da kısa bir ad yaz | +| `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı. `4` Query'de ikisi de **zorunludur** (kolonlar katalogdan okunamaz); wizard/`deploy_wizard` anahtarsız Query ekranını reddeder, tip boşsa (`0`) seçilen alanlardaki karşılığından tamamlar. Ana tablo (en dıştaki `FROM`) `IsDeleted` taşıyorsa `DefaultFilter` yazılır ve sorgu `IsDeleted`'i seçmek zorundadır | | `SelectFieldsDefaultValueJson` | — | **Okuma** sorgusuna parametre olarak katılan değerler (`FieldsDefaultValueDto[]`) — `@USERID`, `@TENANTID` gibi | | `DefaultFilter` | — | Her sorgunun sonuna `AND` ile eklenen sabit `WHERE` koşulu. `DefaultValueHelper` üzerinden geçer, yani `@USERID`/`@TENANTID`/`@NOW` gibi token'lar çözülür. Soft delete'li tabloda (`IsDeletedField: true`) wizard buraya `"IsDeleted" = 'false'` yazar | | `SortMode` | — | **Metin**: `"single"` \| `"multiple"` \| `"none"` (`PlatformConsts.GridOptions.SortMode*`; wizard `single` yazar) | @@ -375,7 +375,7 @@ Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir. | Anahtar | Anlam | | --- | --- | -| `TabType` | Sekmenin görünümü — `ListFormTabTypeEnum`: `List` \| `Card` \| `Tree` \| `Gantt` \| `Scheduler` \| `Todo` \| `Form` \| `Chart` \| `Pivot` | +| `TabType` | Sekmenin görünümü — `ListFormTabTypeEnum`: `List` \| `Card` \| `Tree` \| `Gantt` \| `Scheduler` \| `Todo` \| `Form` \| `Chart` \| `Pivot`. `Form` ana kayda bağlı **tek** kaydı gösterir: kayıt yokken "Yeni" düğmesi (hedef ekranda `AllowAdding` açıksa), kayıt varken yalnızca düzenleme; ikinci kayıt açılmaz | | `TabTitle` | Dil anahtarı | | `Code` | Hedef `ListFormCode` | | `IsRefresh` | Ana kayıt değişince yenilensin | @@ -432,6 +432,10 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, ` satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez. +- Kart sorgusu alt form sekmesinde `Relation` ile süzülmez; bu yüzden alt form olarak üretilen + ekrana widget yazılmaz (menüsüz ya da menülü). Menüsüz (`CreateMenu: false`) wizard'ın `Widgets`'ı + deploy'da ayrıca düşürülür; ekran alt form olarak (`SubForms` sekmesi, `isSubForm`) açıldığında + şerit çizilmez ve yenilenmez. ### 5.4 `LayoutJson` (`LayoutDto`) diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 8ca213a7..e71d35b3 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -259,7 +259,7 @@ bildirilir: | Parametreli SQL kullanımı, yetki kontrolü, tenant izolasyonu | Tartışmaya açık değil | | Depoda okunabilen tablo/kolon/menü/yetki adları | Önce ara, bulduğunu **doğrulat**, sıfırdan sorma | | Var olan bir tanım tablosunun (ülke, para birimi, departman, ünvan, birim) yeniden üretilip üretilmeyeceği | Üretilmez; mevcut tabloya lookup bağlanır (reference §12.6) | -| Liste ekranının üstündeki KPI kartları (`Widgets`) | List ekranı en az 3 kartla gelir, kartlar tablonun durum/bayrak/tarih/tutar kolonlarından seçilir (§4.6.1) | +| Liste ekranının üstündeki KPI kartları (`Widgets`) | List ekranı en az 3 kartla gelir, kartlar tablonun durum/bayrak/tarih/tutar kolonlarından seçilir; alt form olarak üretilen ekrana widget yazılmaz (§4.6.1) | Buna karşılık **her zaman sorulacak** üç şey vardır, çünkü yanlış varsayım geri dönülmez sonuç üretir: **hangi tabloya yazılacağı**, **kimin onaylayacağı** (onay süreci varsa) ve @@ -506,7 +506,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz. | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz | | `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` | -| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | +| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) | | `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) | | `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + analiz + resim içerikleri) | Analizden önce ya da yarım kalan talebe dönmek için; `App.DevRequests` ister | @@ -637,6 +637,11 @@ değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1). Bu yüzden her `create_table` öncesi kolon listesi ekranın editörleriyle karşılaştırılır: editörü `dxImageUpload` olan kolon `nvarchar(MAX)` açılır — tablo kurulduktan sonra kolon değiştirilemez. Kural analizi talebin istediğinden uzaklaştırmaz; yalnızca platformun kurallarına uydurur. +- **Alt form ağacı analizdeki sırayla deploy edilir:** en derindeki alt form önce, onu taşıyan + alt form sonra, ana ekran en son (§0.12 madde 6); ana ekranın `SubForms`'u deploy anında var olan + ekranlara işaret eder ve alt form yetkileri onun altına bu sırada taşınır. Alt form olarak üretilen + her ekran (menüsüz ya da menülü) boş `Widgets` ile, menüsüz olan `CreateMenu: false` ile, `Form` + sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık gider (§4.6). - **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz, `Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır (§0.12). Döngü DB Migrate'i beklemez. @@ -675,11 +680,11 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma. "gerekirse", "ileride" bırakılmaz. - Talebin açıkça yazdığı şey (menü yolu, alan, kapsam) **girdidir**: değiştirilmez, daraltılmaz, alternatifi sunulmaz. -- Talepte olmayan bilgi için analiz karar verir (**Kararlar**, madde 8). Tabloya yazma hedefi, +- Talepte olmayan bilgi için analiz karar verir (**Kararlar**, madde 9). Tabloya yazma hedefi, onaycı ve hesap formülü de boş bırakılmaz: somut karar **(onay)** etiketiyle yazılır; talebi `Ready to Start`'a alan kullanıcı onları onaylamış olur. - Planın her adımının mevcut araçlarla (`create_table`, `deploy_wizard`, `save_component` ve - sözleşmeleri, §4.4) yapılabildiği keşifte doğrulanır (madde 10). + sözleşmeleri, §4.4) yapılabildiği keşifte doğrulanır (madde 11). 1. **Amaç** — talep neden istenmiş, kim kullanacak, hangi işi çözecek (bir-iki cümle). 2. **Görsel analizi** — talepte görsel varsa her biri için (`[image-N]`): ekran türü (liste, form, @@ -712,31 +717,53 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma. analiz satırı `Yetki: {İzin grubu} › {Ara yetki} › {Liste} › {AltForm}` olur. Deploy bunu kendisi kurar — alt form önce deploy edilir, liste deploy edildiğinde alt formun yetkisi listenin altına, listenin alt yetkilerinin (Create/Update/…) ardına taşınır; ayrıca bir alan yazılmaz. + Zincir iç içedir: alt formun alt formu bağlı olduğu alt formun altındadır + (`… › HealthCard › HealthCardGeneral › HealthCardWorkHistory`). Kendi menüsü olan ekran başka bir + ekranın sekmesinde de açılıyorsa yetkisi kendi menü dalında kalır; analiz onu alt form ağacında + "menülü" diye işaretler. Aynı üst menü altındaki ekranlar **bağımlılık sırasıyla** yazılır: başka bir ekranın lookup kaynağı olan (tanım) ekran, onu kullanan ekranın üstündedir. Menüdeki sıra deploy sırasından - gelir; madde 9'daki üretim planı da aynı sırayla yazılır. + gelir; madde 10'daki üretim planı da aynı sırayla yazılır. 5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk, uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı **Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde - olmasa da — ve madde 7 (form alanı + Aktif/Pasif widget'ı) ile madde 9 (`create_table` kolonu, + olmasa da — ve madde 8 (form alanı + Aktif/Pasif widget'ı) ile madde 10 (`create_table` kolonu, demo veride `IsActive: true`) bunu taşır. **Görsel kolonu:** görsel tutan her kolon (fotoğraf, resim, logo, avatar kopyası, belge görüntüsü) `nvarchar(MAX)`'tir ve editörü `dxImageUpload`'dır — değer başka bir kayıttan kopyalanıyor olsa da. Görsel base64 metin olarak saklanır; `nvarchar(512)` gibi sınırlı boy yazılmaz, `dxImageViewer` kullanılmaz (§4.3 editör tablosu). Çok satırlı serbest metin (`dxTextArea`, `dxHtmlEditor`) de `nvarchar(MAX)`'tir. -6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, + **Query kaynağı** (ekran birden çok tabloyu tek sorguda birleştiriyorsa ve view tercih + edilmediyse, §8.2): `SelectCommandType: 4`, `TableName: "Q"`, `KeyFieldName` + + `KeyFieldDbSourceType` zorunlu (§3). Ana tablo (en dıştaki `FROM`'un ilk tablosu) `IsDeleted` + taşıyorsa sorgu ana tablonun `IsDeleted` kolonunu seçer; deploy `DefaultFilter`'ı kendisi yazar. + Query ekranında `DeleteCommand` üretilmez — silme gerekiyorsa hedef tablo kararı **(onay)** ile + yazılır. Join'lerdeki soft delete koşulu (`hc."IsDeleted" = 'false'`) sorgunun içinde durur. +6. **Alt form ağacı** — ekran sekme taşıyorsa her sekme bir satır, kökten iç içe: + + | Sekme | Hedef `ListFormCode` | `TabType` | Menü | İlişki | Ekle / Düzenle / Sil | Yetki yolu | + | --- | --- | --- | --- | --- | --- | --- | + | … | … | `Form` (tek kayıt) / `List` (çok kayıt) | Menüsüz / Menülü | `Id → …Id` | ✓ / ✓ / – | `… › Liste › AltForm` | + + `Form` sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık yazılır (kayıt yoksa sekme ilk + kaydı açar, varsa ikinci kayıt açılmaz); `List` sekmesinde üçü açıktır; silme yalnızca talep + istiyorsa. Alt form olarak üretilen ekranda (menüsüz ya da menülü) widget yoktur, düzenleme formu + tek gruplu ise grup başlığı yazılmaz + (§4.3). Ayrıntı: §4.6. +7. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public yol varsa), var olan ekranlara etkiler. -7. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1), +8. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1), `EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. Görsel alanı her zaman - `dxImageUpload`'dır (madde 5). -8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); + `dxImageUpload`'dır (madde 5). Widget kartları yalnızca ana ekrana yazılır; alt form olarak + üretilen ekrana yazılmaz (§4.6.1). +9. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur. -9. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → onu kullanan ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, +10. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → en derindeki alt form → onu taşıyan alt form → ana ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, `save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur. -10. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı +11. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek eklenene kadar `Ready to Start`'a alınmaz. @@ -810,7 +837,13 @@ yol elle kurulmaz. **`ComponentKind`** (`WizardComponentKindEnum`) — `0` List · `1` Custom **`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query · -`5` StoredProcedure +`5` StoredProcedure. `4` Query'de `TableName` zorunludur (sorgunun alias'ı, `FROM (…) AS "Q"`); +wizard/`deploy_wizard` `"Q"` yazar, elle kurulan Query ekranında da doldurulur. `KeyFieldName` + +`KeyFieldDbSourceType` da zorunludur; wizard anahtarsız Query ekranını reddeder, tip `0` gelirse +seçilen alanlardaki karşılığından tamamlar. Sorgunun ana tablosu (en dıştaki `FROM`'un ilk tablosu) +`IsDeleted` taşıyorsa `DefaultFilter` = `"IsDeleted" = 'false'` yazılır; filtre dış sorguda çalıştığı +için sorgu ana tablonun `IsDeleted` kolonunu seçmelidir, seçmiyorsa deploy reddedilir. `DeleteCommand` +Query'de üretilmez (kaynak tablo adı değildir), gerekiyorsa elle yazılır. **`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService @@ -1725,6 +1758,39 @@ anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + me Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez. Düzenleme tabloya hiç dokunmaz. +**Silme sırası.** Tek tek silmede sıra sonucu belirler; yanlış sıra kayıtları **sahipsiz** bırakır: +bırakılan (`Kept`) satırın sahibi silinen wizard'dı ve sonradan hiçbir silme onu kaldırmaz. + +1. **Alt formlar önce, onları taşıyan ekran sonra** — en derindeki alt form ilk gider + (`HealthCardWorkHistory` → `HealthCardGeneral` → … → `HealthCard`). Bağlı wizard'ı olan wizard + zaten silinemez (yukarıda). +2. **Aynı seviyede en son deploy edilen önce** — paylaşılan menü seviyesi, ara yetki, izin grubu ve + dil anahtarı onu **ilk** üreten wizard'a aittir; o wizard, diğerleri gidip satır kullanımsız + kalınca en son silinirse satır da gider. Önce silinirse satır "kullanımda" diye bırakılır ve + sahipsiz kalır. +3. **Tek tek silmenin sınırı:** alt formun ekranı, onu `SubForms`'unda taşıyan ekran veritabanında + durdukça kullanımda sayılır ve korunur (`Kept`); tablosu da aynı nedenle (`InUse`) düşürülemez. + Kural 1 bu yüzden tek tek silmede alt formun ekranını ve tablosunu temizleyemez — birlikte + üretilmiş bir ekran ağacı **talep olarak** silinir. + +**Talep bazlı toplu silme** (Wizard Manager'da talep başlığındaki çöp kutusu; `DeleteDevRequest`, +`DeleteDevRequestAnalysis`, yetki `App.Listforms.Wizard.Delete`) talebin bütün wizard'larını tek +işlemde siler. Onay diyaloğu sırayı ve tabloları gösterir: +- **Sıra** yukarıdaki kural 1 + 2'dir (`BuildDevRequestDeletePlanAsync`); döngüde kalan dosyalar + yalnızca deploy zamanına göre sıralanır. Talebin dışındaki bir alt forma bağlı olmak silmeyi + durdurmaz — o alt form başka bir talebindir ve yerinde kalır. +- **Talebin ekranları birbirini kullanım saymaz:** alt formun ekranı, aynı işte silinecek üst ekran + yüzünden korunmaz (`WizardRecordCleaner.CleanAsync(seed, batchListForms)`); tablo kullanımında da + talebin ekranları sayılmaz (`WizardTableCleaner.AnalyzeAsync(seed, batchListForms)`). +- **Tablolar en sonda** düşürülür: bütün ekranlar gittikten sonra kullanım yeniden ölçülür; aynı + tabloyu paylaşan alt formlar (`Hr_T_HealthCard` gibi) tabloyu bir kez listeler. Bağlantılar veri + kaynağı kaydı silinmeden önce çözülür. Tablo seçmek yine `App.SqlQueryManager` ister. +- Rapor tek wizard silmeyle aynı sözleşmedir (`WizardDeleteResultDto`, bütün wizard'ların kayıtları + tek listede). + +MCP `delete_wizard` tek dosya siler ve kural 1 + 2'ye uyar; ekran ağacını temizlemek için kullanıcı +Wizard Manager'daki talep silmesini kullanır. + ### 4.6 Alt formlar ve widget'lar **`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar: @@ -1745,6 +1811,27 @@ Düzenleme tabloya hiç dokunmaz. `Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir. +**Alt form ekranının kuralları** (sekmede açılan hedef ekran; MCP ile üretimde de geçerli): + +- **Menü:** Yalnızca sekmede açılan ekran menüsüz (`CreateMenu: false`) üretilir. Kendi sayfası da + olan ekran (Muayene Listesi gibi) menülü kalır ve ana ekranın `SubForms`'una yalnızca eklenir. +- **Yetki:** Menüsüz alt formun okuma yetkisi ana ekranın yetkisinin altındadır + (`ParentName` = ana ekranın yetki kodu). Zincir iç içedir: alt formun alt formu, bağlı olduğu + alt formun altına yazılır (`App.Hr.HealthCard › App.Hr.HealthCardGeneral › App.Hr.HealthCardWorkHistory`). + Alt formlar ana ekrandan **önce** deploy edilir; ana ekran deploy edildiğinde yetkiler altına + taşınır. Deploy'dan sonra `Abp.Identity.Permissions.json`'da bu bağ doğrulanır — kökte kalan alt form + yetkisi rol ekranında grubun köküne düşer. Menülü ekranın yetkisi kendi menü dalında kalır. +- **`TabType: "Form"` — ana kayda bağlı tek kayıt:** Sekme ilişkiye uyan tek kaydı gösterir (1:1, + ör. çalışan başına bir sağlık kartı bölümü). Hedef ekran `AllowAdding: true` ve + `AllowUpdating: true` ile üretilir: kayıt yokken sekme "Yeni" düğmesi gösterir, kayıt varken + yalnızca düzenleme açıktır ve ikinci kayıt açılmaz (`FormView` + `FormButtons`, `isSubForm`). + `AllowAdding` kapalıysa sekme boş "kayıt bulunamadı" mesajında kalır ve kayıt girilemez. + `AllowDeleting` istek açıkça istemedikçe kapalıdır. +- **`TabType: "List"` — ana kayda bağlı çok kayıt:** `AllowAdding`/`AllowUpdating`/`AllowDeleting` + açık gelir; ilişki kolonu (`ChildFieldName`) yeni satıra ana kayıttan yazılır. +- **Widget:** Alt form oluşturulurken widget oluşturulmaz — menüsüz ya da menülü, alt form olarak + üretilen her ekran `Widgets: []` ile gider; sekmede şerit ayrıca çizilmez (§4.6.1). + **`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları. **`Title`, `SubTitle`, `Value`, `Icon`, `Color`, `OnClick` sabit metin değil, `SqlQuery` sonucundaki kolon adlarıdır**; widget, sorgunun döndürdüğü **her satır için bir kart** çizer: @@ -1814,11 +1901,19 @@ gelir; kullanıcı "widget/KPI" demese de, sorulmadan eklenir ve tarifte tek sat - **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal, `yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`, `FaTimesCircle`, `FaCalendarDay`, `FaEye`, `FaMoneyBillWave`…). -- **İstisna — widget eklenmez:** Form ve Chart ekranları; alt form olarak kullanılan menüsüz - ekranlar (`CreateMenu: false`, beğeni/yorum/seçenek gibi detay tabloları); `IsActive` de +- **İstisna — widget eklenmez:** Form ve Chart ekranları; **alt form olarak üretilen her ekran** — + başka bir ekranın `SubForms` sekmesi olan, menüsüz (`CreateMenu: false`, beğeni/yorum/seçenek gibi + detay tabloları) ya da kendi menüsü de olan (Muayene Listesi gibi) — `Widgets: []` ile deploy + edilir; MCP bu ekranlara widget üretmez, menüsüz olanda deploy gönderileni de düşürür; `IsActive` de taşımayan, yalnızca kod + ad tutan eski tanım listeleri. Bunlarda anlamlı bir kırılım yoktur, şerit yalnızca kayıt sayısını tekrarlar. Yeni tanım tablosu `IsActive` ile gelir (§8.1) ve Toplam · Aktif · Pasif kartlarını alır. +- **Alt form sekmesinde widget çizilmez.** Widget sorgusu sekmede `Relation` ile süzülmez; kartlar + sekmedeki satırlarla değil bütün tabloyla ilgili sayılar gösterirdi. Liste görünümleri alt form + olarak açıldığında şeridi çizmez ve yenilemez (`useListFormWidgets(…, isSubForm)`, Pivot'ta + `isSubForm`); kendi menüsü olan ekranın şeridi kendi sayfasında kalır. Bu bir güvencedir, üretim + kuralının yerine geçmez: alt form olarak üretilen ekrana yine widget yazılmaz. Ana kayda göre + sayı isteniyorsa kart ana ekranın widget'ına yazılır. - Kartların sorgusunu seed dosyasına yazmadan önce tablo kolonları doğrulanır (MCP'de `search_schema`, ekranda tablo tasarımcısı). Var olmayan kolona yazılmış kart ekranı hatayla açar. @@ -1924,8 +2019,8 @@ olmalıdır. | --- | --- | | `DataSourceCode` | Bağlantı | | `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi | -| `TableName` | Tablo adı/alias — yazma komutlarının hedefi | -| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar | +| `TableName` | Tablo adı/alias — yazma komutlarının hedefi; `4` Query'de zorunlu alias (`"Q"`) | +| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar; `4` Query'de ikisi de zorunlu | | `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler | | `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) | | `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri | @@ -4410,6 +4505,9 @@ Prompt ile üretim yaptıysan ek olarak: - [ ] Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` açık, `DefaultLayout: "grid"`; kapalı olan görünüm yalnızca istek onu açıkça dışarıda bıraktığı için kapalı (§9.8). - [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8). +- [ ] Alt form olarak üretilen her ekranda (menüsüz ya da menülü) `Widgets` boş (§4.6.1 istisna). +- [ ] Menüsüz alt formun yetkisi ana ekranın (ya da bağlı olduğu alt formun) yetkisinin altında; + `Form` sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık (§4.6). - [ ] List ekranında en az 3 kartlık `Widgets` var (istisna değilse); kiracı tablosunda her kart `"TenantId" = '@TENANTID'` taşıyor, başlıklar dil anahtarı, kolonlar doğrulandı (§4.6.1). - [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde diff --git a/README.md b/README.md index f427acc5..01a72a03 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,7 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl | `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). | | `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. | | `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). | -| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. | +| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. Alt form olarak üretilen ekrana (menüsüz ya da menülü) yazılmaz; menüsüz wizard'da deploy boşaltır, alt form sekmesinde çizilmez. | | `WorkflowJson` | İş akışı bağlaması. | | `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu. | | `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` | Alternatif görünümlerin ayarları. | @@ -754,7 +754,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: | **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`. | | **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). | | **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. | -| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Wizard'ı üreten talep listede görünür ve kaydına bağlanır; talebin bütün wizard'ları tek arşivde export edilir (`export-dev-request`). Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | +| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Menüsüz alt formlar onları sekme olarak açan ekranın altında iç içe görünür; kendi menüsü olan ekran başka bir ekranın sekmesi olsa da kökte kalır (export kapanışı onu yine taşır). Wizard'ı üreten talep listede görünür ve kaydına bağlanır; talebin bütün wizard'ları tek arşivde export edilir (`export-dev-request`) ya da bağımlılık sırasıyla tek işlemde silinir (`delete-dev-request`; alt formlar önce, talebin ekranları birbirini kullanım saymaz, tablolar en sonda — `lowcode.instructions.md` §4.5). Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | > Bu ekranların artefakt şemaları, enum değerleri ve çalışan örnekleri > [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs index 2ed76b64..4295bc7b 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs @@ -15,6 +15,12 @@ public interface IListFormWizardAppService /// Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur. Task DeleteAnalysis(string fileName); + /// Bir gelistirme talebinin butun wizard'larinin silinme sirasi ve secilebilen tablolari. + Task DeleteDevRequestAnalysis(Guid devRequestId); + + /// Bir gelistirme talebinin butun wizard'larini bagimlilik sirasiyla tek islemde siler. + Task DeleteDevRequest(WizardDeleteDevRequestInput input); + /// Izin gruplarini iki dilli gorunen adlariyla dondurur. Task> GetPermissionGroups(); 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 945dd083..094f4ad2 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -253,10 +253,12 @@ public class ListFormWizardDto IsOrganizationUnit = IsOrganizationUnit, SelectCommandType = SelectCommandType, SelectCommand = SelectCommand, + TableName = SelectCommandType == SelectCommandTypeEnum.Query ? WizardConsts.QuerySourceAlias : null, KeyFieldName = KeyFieldName, KeyFieldDbSourceType = KeyFieldDbSourceType, DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null, - DeleteCommand = isDeletedField ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null, + // 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, 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 f8ce149c..852cae6d 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -532,6 +532,31 @@ public class WizardDeleteInput public List DropTables { get; set; } = []; } +/// Talebin wizard'lari silinme sirasiyla; alt formlar onlari tasiyan ekrandan once gelir. +public class WizardDeleteDevRequestAnalysisDto +{ + public Guid DevRequestId { get; set; } + public string Title { get; set; } + public List Files { get; set; } = []; + + /// Talebin disindaki ekranlar hesaba katilarak secilebilen tablolar; ayni tablo bir kez yer alir. + public List Tables { get; set; } = []; +} + +public class WizardDeleteOrderItemDto +{ + public string FileName { get; set; } + public string WizardName { get; set; } +} + +public class WizardDeleteDevRequestInput +{ + public Guid DevRequestId { get; set; } + + /// Onayda secilen tablolar; yalnizca analizde olanlar dusurulur. + public List DropTables { get; set; } = []; +} + /// Wizard seed dosyası özet bilgisi (yönetim listesi için). public class WizardFileInfoDto { diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 9a5c9853..cde40093 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -125,6 +125,12 @@ public class ListFormWizardAppService( // Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim // kalan bir deploy mevcut menu/permission satirlarini bozabilir. ValidateWizardName(wizardName); + ValidateQueryKeyField(input); + + // Menusuz ekran yalnizca baska bir ekranin alt formu olarak acilir; orada widget cizilmez + // ve sorgusu ana kayda gore suzulmedigi icin kartlar yaniltici sayilar gosterirdi. + if (!input.CreateMenu) + input.Widgets = []; await ValidateWizardUniquenessAsync(wizardName, code, input.ResolveMenuParentChain(), editFileName, previous, rows); await ValidateWidgetLanguageKeysAsync(input, rows); @@ -230,7 +236,9 @@ public class ListFormWizardAppService( // Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar. var tableColumns = await GetTableColumnsAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); - isDeleted = tableColumns.ContainsKey("IsDeleted"); + isDeleted = input.SelectCommandType == SelectCommandTypeEnum.Query + ? await IsQueryMainTableSoftDeletedAsync(input) + : tableColumns.ContainsKey("IsDeleted"); isCreated = tableColumns.ContainsKey("CreatorId"); ValidateImageColumnLengths(input, tableColumns); listFormDto = input.BuildListForm(isDeleted, isCreated); @@ -1029,6 +1037,146 @@ public class ListFormWizardAppService( }; } + /// + /// Talebin silinme sirasi ve onayda sorulacak tablolar; hicbir seye dokunmaz. Tablolarin + /// kullanim kontrolu talebin kendi ekranlarini disarida birakir, hepsi ayni islemde gider. + /// + [Authorize(AppCodes.Listforms.Wizards.Delete)] + [HttpGet("api/app/list-form-wizard/delete-dev-request-analysis")] + public async Task DeleteDevRequestAnalysis(Guid devRequestId) + { + var plan = await BuildDevRequestDeletePlanAsync(devRequestId); + + return new WizardDeleteDevRequestAnalysisDto + { + DevRequestId = devRequestId, + Title = plan.Title, + Files = [.. plan.Files.Select(f => new WizardDeleteOrderItemDto + { + FileName = f.FileName, + WizardName = f.Seed.Wizard?.WizardName ?? f.FileName, + })], + Tables = await AnalyzeBatchTablesAsync(plan), + }; + } + + /// + /// Talebin butun wizard'larini tek tek silmenin yol actigi yarim temizligi onler. Tek tek + /// silmede alt form, onu tasiyan ekran hala durdugu icin kullanimda sayilip korunur; ekran + /// sonradan silinince alt formun kayitlari ve tablosu sahipsiz kalir. Burada wizard'lar + /// bagimlilik sirasiyla silinir (), talebin kendi + /// ekranlari birbirini kullanim saymaz ve tablolar ekranlar gittikten sonra dusurulur. + /// + [UnitOfWork] + [Authorize(AppCodes.Listforms.Wizards.Delete)] + [HttpPost("api/app/list-form-wizard/delete-dev-request")] + public async Task DeleteDevRequest(WizardDeleteDevRequestInput input) + { + Check.NotNull(input, nameof(input)); + + var dropTables = input.DropTables?.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).ToList() ?? []; + if (dropTables.Count > 0) + await AuthorizationService.CheckAsync(AppCodes.SqlQueryManagers.Default); + + var plan = await BuildDevRequestDeletePlanAsync(input.DevRequestId); + + // Baglantilar, wizard'in kurdugu veri kaynagi kaydi silinmeden once cozulur. + var before = await AnalyzeBatchTablesAsync(plan); + var connections = await _tableCleaner.ResolveConnectionsAsync( + before.Where(t => dropTables.Contains(t.TableName, StringComparer.OrdinalIgnoreCase))); + + var result = new WizardDeleteResultDto { FileName = input.DevRequestId.ToString(), WizardName = plan.Title }; + foreach (var (fileName, _) in plan.Files) + result.Items.AddRange((await DeleteFileInternalAsync(fileName, batchListForms: plan.ListFormCodes)).Items); + + // Ekranlar gittikten sonra kullanim yeniden olculur; veri kaynagi silinmis olabilecegi icin + // tablonun varligi ilk analizden alinir. + var tables = await AnalyzeBatchTablesAsync(plan); + foreach (var table in tables) + { + var first = before.FirstOrDefault(t => SameTable(t, table)); + if (first is { Exists: true } && !table.Exists) + { + table.Exists = true; + table.RowCount = first.RowCount; + table.SchemaName = first.SchemaName; + } + } + + if (tables.Count > 0) + await _tableCleaner.DropAsync(tables, dropTables, connections, result.Items); + + return result; + } + + private sealed record DevRequestDeletePlan(string Title, List<(string FileName, WizardSeedFileDto Seed)> Files, List ListFormCodes); + + /// + /// Talebin wizard'lari silinme sirasiyla. Alt formlar onlari tasiyan ekrandan once gelir + /// (tek tek silmedeki kuralla ayni, ); siralanabilen + /// adaylar arasinda en son deploy edilen once silinir. Paylasilan menu seviyesi, ara yetki ve dil + /// anahtari onu ilk ureten wizard'a aittir; digerleri gittikten sonra kullanimsiz kalip silinir. + /// + private async Task BuildDevRequestDeletePlanAsync(Guid devRequestId) + { + var members = (await ReadSeedFilesAsync()) + .Where(f => f.Seed?.Wizard != null && f.Seed.DevRequest?.Id == devRequestId) + .Select(f => (f.FileName, Seed: f.Seed!)) + .ToList(); + if (members.Count == 0) + throw new UserFriendlyException("Bu talebe bağlı wizard dosyası yok."); + + var dependencies = (await GetFiles()).ToDictionary(f => f.FileName, f => f.Dependencies, StringComparer.OrdinalIgnoreCase); + var remaining = members.ToList(); + var ordered = new List<(string FileName, WizardSeedFileDto Seed)>(); + + bool IsReady((string FileName, WizardSeedFileDto Seed) file) => + !(dependencies.GetValueOrDefault(file.FileName) ?? []) + .Any(d => remaining.Any(r => string.Equals(r.FileName, d, StringComparison.OrdinalIgnoreCase))); + + while (remaining.Count > 0) + { + // Donguye giren dosyalarin hicbiri hazir olmaz; o durumda sira yalnizca deploy zamanina duser. + var ready = remaining.Where(IsReady).ToList(); + var next = (ready.Count > 0 ? ready : remaining).OrderByDescending(f => f.Seed.Wizard.SeededAt).First(); + + ordered.Add(next); + remaining.Remove(next); + } + + var devRequest = await repoDevRequest.FindAsync(devRequestId); + var title = devRequest?.Title ?? members[0].Seed.DevRequest.Title; + var listFormCodes = members + .Where(f => f.Seed.Wizard.ComponentKind != WizardComponentKindEnum.Custom) + .Select(f => f.Seed.Wizard.ResolveListFormCode()) + .ToList(); + + return new DevRequestDeletePlan(title, ordered, listFormCodes); + } + + /// Talebin ekranlarinin tablolari, ayni tablo bir kez; bir ekran icin secilemeyen tablo secilemez kalir. + private async Task> AnalyzeBatchTablesAsync(DevRequestDeletePlan plan) + { + var tables = new List(); + foreach (var (_, seed) in plan.Files) + { + foreach (var table in await _tableCleaner.AnalyzeAsync(seed, plan.ListFormCodes)) + { + var existing = tables.FirstOrDefault(t => SameTable(t, table)); + if (existing == null) + tables.Add(table); + else if (existing.CanDrop && !table.CanDrop) + tables[tables.IndexOf(existing)] = table; + } + } + + return tables; + } + + private static bool SameTable(WizardDeleteTableDto a, WizardDeleteTableDto b) => + string.Equals(a.DataSourceCode, b.DataSourceCode, StringComparison.OrdinalIgnoreCase) && + string.Equals(a.TableName, b.TableName, StringComparison.OrdinalIgnoreCase); + /// /// Bagli wizard'lari (alt formlar / component bagimliliklari) olan bir wizard silinemez; once /// bagli olanlar silinir. Kural listedeki gruplamayla ayni kaynaktandir @@ -1060,7 +1208,11 @@ public class ListFormWizardAppService( /// satirlari cikarilir. Veritabani adimi basarisizsa dosyalara dokunulmaz. /// /// - private async Task DeleteFileInternalAsync(string fileName, List? dropTables = null) + /// + /// Toplu silmede ayni islemde silinen ekranlar (). Verildiginde + /// tablolara dokunulmaz; toplu silme onlari ekranlar gittikten sonra kendisi dusurur. + /// + private async Task DeleteFileInternalAsync(string fileName, List? dropTables = null, IReadOnlyCollection? batchListForms = null) { var filePath = ResolveWizardSeedFilePath(fileName); @@ -1075,7 +1227,7 @@ public class ListFormWizardAppService( // Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida // birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur. - var tables = seed != null + var tables = seed != null && batchListForms == null ? await _tableCleaner.AnalyzeAsync(seed) : []; var connections = await _tableCleaner.ResolveConnectionsAsync( @@ -1091,7 +1243,7 @@ public class ListFormWizardAppService( List items; using (var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(isTransactional: true), requiresNew: true)) { - items = await _recordCleaner.CleanAsync(seed); + items = await _recordCleaner.CleanAsync(seed, batchListForms); await uow.CompleteAsync(); } @@ -2676,6 +2828,96 @@ public class ListFormWizardAppService( } } + /// + /// Query kaynaginin kolonlari katalogdan okunamaz; anahtar alan ve tipi yalnizca girdiden gelir. + /// Anahtarsiz ekranda sayim, siralama ve satir secimi bozulur. Tip bos (0) gelirse secilen + /// alanlardaki karsiligindan tamamlanir. + /// + private static void ValidateQueryKeyField(ListFormWizardDto input) + { + if (input.ComponentKind == WizardComponentKindEnum.Custom || input.SelectCommandType != SelectCommandTypeEnum.Query) + return; + + if (string.IsNullOrWhiteSpace(input.KeyFieldName)) + throw new UserFriendlyException("Query kaynaklı ekranda anahtar alan (KeyFieldName) zorunludur."); + + input.KeyFieldName = input.KeyFieldName.Trim(); + if (input.KeyFieldDbSourceType != default) + return; + + var keyItem = (input.Groups ?? []) + .SelectMany(g => g.Items) + .FirstOrDefault(i => string.Equals(i.FieldName, input.KeyFieldName, StringComparison.OrdinalIgnoreCase)); + if (keyItem == null || keyItem.DbSourceType == default) + throw new UserFriendlyException($"Query kaynaklı ekranda '{input.KeyFieldName}' anahtar alanının tipi (KeyFieldDbSourceType) zorunludur."); + + input.KeyFieldDbSourceType = keyItem.DbSourceType; + } + + /// + /// Query kaynaginin ana tablosu (en distaki FROM'un ilk tablosu) soft delete tasiyorsa silinen + /// satirlar listeye dusmesin diye DefaultFilter yazilir. Filtre turetilmis tablonun disinda + /// calistigi icin sorgu IsDeleted kolonunu secmek zorundadir; secmiyorsa deploy reddedilir. + /// + private async Task IsQueryMainTableSoftDeletedAsync(ListFormWizardDto input) + { + var (selectList, mainTable) = SplitTopLevelQuery(input.SelectCommand); + if (mainTable == null) + return false; + + var columns = await GetTableColumnsAsync(input.DataSourceCode, SelectCommandTypeEnum.Table, mainTable); + if (!columns.ContainsKey("IsDeleted")) + return false; + + if (!Regex.IsMatch(selectList, @"\bIsDeleted\b", RegexOptions.IgnoreCase)) + throw new UserFriendlyException($"Query kaynağının ana tablosu '{mainTable}' soft delete taşıyor; silinen satırların listelenmemesi için sorgu ana tablonun IsDeleted kolonunu seçmelidir."); + + return true; + } + + /// + /// Sorgunun en dis seviyedeki select listesi ve FROM'dan sonraki ilk tablo. Alt sorgu ve metin + /// sabitlerindeki FROM sayilmaz; kaynak turetilmis tabloysa ana tablo yoktur. + /// + private static (string SelectList, string? MainTable) SplitTopLevelQuery(string sql) + { + if (string.IsNullOrWhiteSpace(sql)) + return (string.Empty, null); + + var depth = 0; + var inLiteral = false; + for (var i = 0; i < sql.Length; i++) + { + var c = sql[i]; + if (c == '\'') + inLiteral = !inLiteral; + else if (inLiteral) + continue; + else if (c == '(') + depth++; + else if (c == ')') + depth--; + else if (depth == 0 && IsKeywordAt(sql, i, "FROM")) + { + var match = Regex.Match(sql[(i + 4)..], @"^\s+([\w\.\[\]""]+)"); + return (sql[..i], match.Success ? match.Groups[1].Value : null); + } + } + + return (sql, null); + } + + private static bool IsKeywordAt(string sql, int index, string keyword) + { + if (index + keyword.Length > sql.Length + || string.Compare(sql, index, keyword, 0, keyword.Length, StringComparison.OrdinalIgnoreCase) != 0) + return false; + + var before = index == 0 ? ' ' : sql[index - 1]; + var after = index + keyword.Length == sql.Length ? ' ' : sql[index + keyword.Length]; + return !char.IsLetterOrDigit(before) && before != '_' && !char.IsLetterOrDigit(after) && after != '_'; + } + /// Tablonun kolonlari ve metin kolonlarinin karakter siniri (sinirsizsa -1 ya da bos). private async Task> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) { diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs index 28035e21..0995ccaf 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs @@ -64,7 +64,11 @@ public class WizardRecordCleaner( /// Raporda bir kaydi kullananlarin en fazla bu kadari listelenir. private const int MaxUsedBy = 20; - public async Task> CleanAsync(WizardSeedFileDto seed) + /// + /// Ayni islemde silinen ekranlar; onlarin alt form baglari bu ekrani kullanim saymaz. Toplu + /// silmede alt form, onu tasiyan ekrandan once silinir ve bag o ana kadar durur. + /// + public async Task> CleanAsync(WizardSeedFileDto seed, IReadOnlyCollection? batchListForms = null) { var report = new List(); var wizard = seed.Wizard; @@ -72,11 +76,11 @@ public class WizardRecordCleaner( // Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin. if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode)) - await CleanListFormAsync(wizard.ListFormCode, report); + await CleanListFormAsync(wizard.ListFormCode, report, batchListForms); // Migrate edilmeden kodu degistirilen ekranin onceki surumu hala kuruludur. foreach (var listFormCode in seed.Pending?.ListFormCodes ?? []) - await CleanListFormAsync(listFormCode, report); + await CleanListFormAsync(listFormCode, report, batchListForms); var deletedMenus = await CleanMenusAsync(ins.MenuCodes, report); var adoptedLanguageKeys = new List(); @@ -123,7 +127,7 @@ public class WizardRecordCleaner( /// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte /// kalir, aksi halde ust ekranin sekmesi bozulurdu. /// - private async Task CleanListFormAsync(string listFormCode, List report) + private async Task CleanListFormAsync(string listFormCode, List report, IReadOnlyCollection? batchListForms) { var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode); @@ -136,8 +140,10 @@ public class WizardRecordCleaner( else { var quoted = Quote(listFormCode); - var parents = await repoListForm.GetListAsync(a => - a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted)); + var parents = (await repoListForm.GetListAsync(a => + a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted))) + .Where(p => batchListForms?.Contains(p.ListFormCode, StringComparer.OrdinalIgnoreCase) != true) + .ToList(); if (parents.Count > 0) { diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs index 001adf16..6be25802 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs @@ -43,7 +43,8 @@ public class WizardTableCleaner( { private const int MaxUsedBy = 20; - public async Task> AnalyzeAsync(WizardSeedFileDto seed) + /// Ayni islemde silinen ekranlar; tabloyu kullanmalari engel sayilmaz. + public async Task> AnalyzeAsync(WizardSeedFileDto seed, IReadOnlyCollection? batchListForms = null) { var wizard = seed?.Wizard; if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom) @@ -76,7 +77,10 @@ public class WizardTableCleaner( await ReadTableStateAsync(table); table.SchemaName ??= "dbo"; - var usedBy = await FindUsagesAsync(table, listFormCode); + var usedBy = (await FindUsagesAsync(table, listFormCode)) + .Where(u => u.RecordType != WizardRecordTypeEnum.ListForm || + batchListForms?.Contains(u.Name, StringComparer.OrdinalIgnoreCase) != true) + .ToList(); if (usedBy.Count > 0) { table.Reason = WizardDeleteReasonEnum.InUse; diff --git a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs index 35b4d1ba..b7aea5b7 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs @@ -136,6 +136,12 @@ public static class WizardConsts LookupQuery = lookupQuery, }); + /// + /// Query kaynagi turetilmis tablo olarak sarilir ve kolonlar bu takma adla nitelenir; + /// TableName bos kalirsa takma ad olarak sorgu metninin kendisi kullanilir. + /// + public const string QuerySourceAlias = "Q"; + public static string DefaultDeleteCommand(string tableName) { return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id"; diff --git a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs index 07992ed6..f9980ae4 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs @@ -88,6 +88,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager private string From { get { return SelectCommandType == SelectCommandTypeEnum.Query ? $"({SelectCommand})" : SelectCommand; } } private SelectCommandTypeEnum SelectCommandType { get; set; } private string KeyFieldName { get; set; } + private DbType KeyFieldDbSourceType { get; set; } private List JoinParts { get; set; } private List WhereParts { get; set; } private List SortParts { get; set; } @@ -154,6 +155,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager SelectCommand = DefaultValueHelper.GetDefaultValue(listform.SelectCommand); TableName = listform.TableName.IsNullOrEmpty() ? SelectCommand : listform.TableName; KeyFieldName = listform.KeyFieldName; + KeyFieldDbSourceType = listform.KeyFieldDbSourceType; SelectCommandType = listform.SelectCommandType; var KeyField = new SelectField { @@ -221,6 +223,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager SelectCommand = null; TableName = null; KeyFieldName = null; + KeyFieldDbSourceType = default; SelectFields = []; JoinParts = []; WhereParts = []; @@ -296,7 +299,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager private string GetSelectString() { - return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM \"{From}\" AS \"{TableName}\""; + return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM {From} AS \"{TableName}\""; } private string SetJoinStringFromCustomizations(List listFormCustomizations) @@ -547,9 +550,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager var filterParamKey = $"@{filterFieldName}{customKey}"; var filterParamKeyLower = filterParamKey; var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName); - if (listFormField == null) + // Anahtar alan her sorguya eklenir ama alan listesinde olmayabilir; grid satira odaklanirken + // filtreye anahtar kosulu yazar. + var isKeyField = listFormField == null && filterFieldName == KeyFieldName; + if (listFormField == null && !isKeyField) { - return sqlPart; + // Kosul dusurulurse etrafindaki baglac ve parantezler yetim kalir (WHERE and (...))); + // yerine parantezleri koruyan etkisiz bir kosul yazilir. + var leading = fieldName[..^fieldName.TrimStart('(').Length]; + var trailing = fieldValue[fieldValue.TrimEnd(')').Length..]; + return $"{leading}1 = 1{trailing}"; } var filterOperator = op.RemoveParentheses(); @@ -558,7 +568,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager DbType dbType = filterGroupInterval switch { "year" or "quarter" or "month" or "day" or "dayOfWeek" => DbType.Int32, - _ => listFormField.SourceDbType + _ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType }; var filterValue = fieldValue.RemoveParentheses(); @@ -689,7 +699,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager private string GetTotalCountQuery() { - var sql = $"SELECT COUNT(\"{TableName}\".\"{KeyFieldName}\") FROM \"{From}\" AS \"{TableName}\""; + var sql = $"SELECT COUNT(\"{TableName}\".\"{KeyFieldName}\") FROM {From} AS \"{TableName}\""; sql += GetJoinString(); sql += GetWhereString(); @@ -698,7 +708,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager private string GetSummaryQuery(string fieldName, string summaryType) { - var sql = $"SELECT {summaryType}({fieldName}) FROM \"{From}\" AS \"{TableName}\""; + var sql = $"SELECT {summaryType}({fieldName}) FROM {From} AS \"{TableName}\""; sql += GetJoinString(); sql += GetWhereString(); @@ -767,7 +777,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager sqlSub.Append(string.Join(',', GroupTuples.Select((a) => $"{a.Field} AS \"{a.SelectExpr}\""))); sqlSub.Append(string.Join("", GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\""))); sqlSub.Append(",COUNT(1) AS \"Summary_Count\""); - sqlSub.Append($" FROM \"{From}\" AS \"{TableName}\""); + sqlSub.Append($" FROM {From} AS \"{TableName}\""); sqlSub.Append(GetJoinString()); sqlSub.Append(GetWhereString()); sqlSub.Append($" GROUP BY {string.Join(',', GroupTuples.Select((a) => a.Field))}"); diff --git a/claude.md b/claude.md index 1fd11241..71697cd6 100644 --- a/claude.md +++ b/claude.md @@ -171,7 +171,14 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → sorulmadan **en az 3 kartlık** `Widgets` ile gelir; kartlar tablonun durum / boolean bayrak / tarih / tutar kolonlarından seçilir, başlıklar dil anahtarıdır (`App.Widget.*` önce). Kiracı tablosunda her kart `"TenantId" = '@TENANTID'` taşır; `IsDeleted` koşulu yazılmaz. Form/Chart, - menüsüz alt form ve basit tanım listeleri istisnadır. Ayrıntı: `lowcode.instructions.md` §4.6.1. + alt form ve basit tanım listeleri istisnadır. **Alt form oluşturulurken widget oluşturulmaz**: + başka bir ekranın `SubForms` sekmesi olarak üretilen her ekran — menüsüz ya da kendi menüsü olan — + `Widgets: []` ile deploy edilir. Deploy menüsüz (`CreateMenu: false`) wizard'ın `Widgets`'ını + ayrıca düşürür; ekran alt form sekmesinde açıldığında şerit de çizilmez + (`useListFormWidgets(…, isSubForm)`) — kart sorgusu ana kayda göre süzülmez. Ayrıntı: + `lowcode.instructions.md` §4.6.1. + Alt form ekranının menü/yetki/düzenleme kuralları (menüsüz alt formun yetkisi ana ekranın altında, + `Form` sekmesi tek kayıt + `AllowAdding`/`AllowUpdating` açık): `lowcode.instructions.md` §4.6. ### Cross-cutting: touching one thing means touching these too diff --git a/configs/seeds/host/data/App.DevRequests/ce4269ad-8d08-487a-9e36-c002dd482a3c.json b/configs/seeds/host/data/App.DevRequests/ce4269ad-8d08-487a-9e36-c002dd482a3c.json index 7799bd21..c3874d5f 100644 --- a/configs/seeds/host/data/App.DevRequests/ce4269ad-8d08-487a-9e36-c002dd482a3c.json +++ b/configs/seeds/host/data/App.DevRequests/ce4269ad-8d08-487a-9e36-c002dd482a3c.json @@ -9,18 +9,18 @@ "TenantId": null, "UserName": "system@sozsoft.com", "RequestDate": null, - "Status": "Backlog", + "Status": "Testing", "Title": "Human Resources -> Sağlık", - "Description": "

Bu eklenecekler Human Resources -> Sağlık alt menüsünün içerisine eklenecektir.


İstirahat Listesi : Çalışanların istirahat listesi ekranıdır. İstirahat Tipi: Hastalık, İş Kazası, Analık, Meslek Hastalığı gibi seçilebilmelidir.



Muayene Listesi : Çalışanların Muayene Listesidir. Her kullanıcının periyodik veya plansız şekilde yapılan muayene sonuçlarının listesidir.


Sağlık Kartları: Her çalışanın detaylı şekilde sağlık bilgilerinin yer aldığı ekrandır. Bu ekranın içerisinde 9 farklı tab içerisinde ayrı bilgiler içermektedir. Aşağıda bu tabların resimlerini gönderiyorum. Bu bilgiler Kullanıcı Detail ekranında görünmeli. http://localhost:3000/admin/users/detail/61243cdf-045d-ee58-76df-3a2403667ea8 bu url de olduğu komponentte güncellenebilmeli.


Hasta Kayıt Defteri: Kullanıcıların hasta olduklarında doldurdukları listedir. Tip sütununda Hastalık, İş Kazası Dönüş, İstirahat Dönüş, Sonuç kısmında Tedavi (Taburcu), Sevk ve İstirahat seçenekleri olmalı. Sevk olaunca Sevk Yeri açılıyor.

", + "Description": "

Bu eklenecekler Human Resources -> Sağlık alt menüsünün içerisine eklenecektir.


İstirahat Listesi : Çalışanların istirahat listesi ekranıdır. İstirahat Tipi: Hastalık, İş Kazası, Analık, Meslek Hastalığı gibi seçilebilmelidir.



Muayene Listesi : Çalışanların Muayene Listesidir. Her kullanıcının periyodik veya plansız şekilde yapılan muayene sonuçlarının listesidir.


Sağlık Kartları: Her çalışanın detaylı şekilde sağlık bilgilerinin yer aldığı ekrandır. bu liste Kullanıcı listesinin bir benzeri olabilir. Detay formuna geçince aşağıdaki resimlere ait detay formu açılmalı. Bu ekranın içerisinde 9 farklı tab içerisinde ayrı bilgiler içermektedir. Aşağıda bu tabların resimlerini gönderiyorum. Genel, Fiziki Muayeene, Anamnez1, Anamnez2,Anamnez3, Sonuç komponentlerine AllowAdding true olamayacağı için bu komponentlerdeki değerleri AbpUsers tablosuna extension olarak tanımlanabilir. Ayrıca Anamnez1, Anamnez2, Anamnez3 tek bir komponent olabilir


Hasta Kayıt Defteri: Kullanıcıların hasta olduklarında doldurdukları listedir. Tip sütununda Hastalık, İş Kazası Dönüş, İstirahat Dönüş, Sonuç kısmında Tedavi (Taburcu), Sevk ve İstirahat seçenekleri olmalı. Sevk olaunca Sevk Yeri açılıyor.

", "DueDate": "2026-09-30T13:58:00", "Tags": "HR", "Assignees": "system@sozsoft.com", "Priority": "5", "Completed": false, "SortOrder": 0, - "Timesheets": null, - "SubTasks": null, - "Analysis": null + "Timesheets": "[{\"id\":\"a0f1c316-3b52-ac49-ca30-3a2405b7b8f3\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0333},{\"id\":\"d663f14d-57b6-d290-ce78-3a2405bb7226\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0667},{\"id\":\"685109c1-e679-2ca0-c21d-3a2405bd6448\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCard tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"3349a545-65e0-36f1-b443-3a2405bd87a6\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCardWorkHistory tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"3283ba27-2815-0a37-fa80-3a2405bda28a\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCardFile tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"572dca14-6b18-4b36-8ac7-3a2405bdb890\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_SickLeave tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"7ad38b49-606b-033b-c7d1-3a2405bdd3e0\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_Examination tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"3634fb91-dd88-4951-f3a7-3a2405bdec00\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_PatientRecord tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"5770d408-569c-0f71-7abe-3a2405be7cf9\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"SickLeave.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"2519bbc8-6be8-ef13-2997-3a2405bf22c2\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Examination.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"691db3b2-f75f-c2ed-9648-3a2405bfbce6\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"PatientRecord.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"f7c9f315-9792-b344-15fc-3a2405bffd30\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardWorkHistory.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"535798e6-3fbb-e458-0116-3a2405c0d1c9\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardGeneral.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"a49d4cf7-99c5-c1d8-35a5-3a2405c17584\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardLab.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"ed516c35-566b-82ea-1096-3a2405c1f744\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardAnamnesis1.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"dad5e5b1-6f48-f242-1f0e-3a2405c25d1d\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardAnamnesis2.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"abce155b-4f3d-6aa1-d414-3a2405c2adb3\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardAnamnesis3.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"0be91fd3-2280-f0df-e54b-3a2405c31afb\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardResult.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"2219a38c-8bab-c330-c3d4-3a2405c364ab\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardFile.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"0de44dac-3f31-5f09-e3ad-3a2405c3ef87\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCard.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"a8b69fb8-04e0-42e4-142b-3a2405c4190f\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"18 tamamlanm\\u0131\\u015F alt g\\u00F6rev eklendi\",\"duration\":0.0167},{\"id\":\"8fd317f8-4bcb-08c9-e34d-3a2405c44b71\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Analize \\u00FCretim notu eklendi\",\"duration\":0.0167},{\"id\":\"d8ad0c50-8496-9565-ef81-3a2405c45a28\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"App.DevRequests ekran\\u0131nda sat\\u0131r g\\u00FCncellendi (Status)\",\"duration\":0.0167},{\"id\":\"60efca67-11b3-78bf-34a0-3a24061f241b\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.05},{\"id\":\"3dcb7d4d-1bdb-3c59-7dc8-3a2406238d53\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCard tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"d45e423e-35e6-bb6f-34de-3a240623a358\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCardWorkHistory tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"84f3b35f-92ab-18e3-13e6-3a240623b2ba\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_HealthCardFile tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"1962ea16-4d2d-a91d-4673-3a240623cf6a\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_SickLeave tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"2bf4eb00-b49a-7275-42db-3a240623f162\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_Examination tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"9138c393-bd6d-781b-5958-3a24062409a4\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_PatientRecord tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"dd8a7377-3aee-af5c-7aff-3a240624a081\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"SickLeave.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"0daeeb54-3fc0-0e88-0b9d-3a240625497a\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Examination.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"d46ffaa7-220d-8b29-d473-3a240625f34d\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"PatientRecord.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"998865ba-ae46-00e1-5093-3a240626d986\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardGeneral.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"76e098bc-c4f6-b3cc-7cda-3a240627197b\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardWorkHistory.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"aefc6477-e471-6f99-500b-3a240627b7d2\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardLab.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"0c958304-53bf-d0d0-d661-3a2406289476\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardAnamnesis.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"320e67ad-77e6-06e8-9ad5-3a2406290827\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardResult.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"b007d7d2-254d-0069-f81d-3a24062952bf\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCardFile.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"5dc4c721-ff5a-f9c9-f4c4-3a240629d9aa\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"HealthCard.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"56de254a-35c5-54eb-107e-3a240629ffd0\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"16 tamamlanm\\u0131\\u015F alt g\\u00F6rev eklendi\",\"duration\":0.0167},{\"id\":\"0965b809-3df6-b8a2-190c-3a24062a390a\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Analize \\u00FCretim notu eklendi\",\"duration\":0.0167}]", + "SubTasks": "[{\"id\":\"6123e46d-7e8e-2869-0c0a-3a2405c418c2\",\"title\":\"create_table: Hr_T_HealthCard\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"3a161677-a10d-6530-b6c5-3a2405c418c2\",\"title\":\"create_table: Hr_T_HealthCardWorkHistory\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"156a5e00-fea2-49ed-0f32-3a2405c418c2\",\"title\":\"create_table: Hr_T_HealthCardFile\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"9a0011a5-77b0-e335-5005-3a2405c418c2\",\"title\":\"create_table: Hr_T_SickLeave\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"fdd4b23d-3f39-a2eb-8727-3a2405c418c2\",\"title\":\"create_table: Hr_T_Examination\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"b80e1f42-1610-b6fd-e5ac-3a2405c418c2\",\"title\":\"create_table: Hr_T_PatientRecord\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"90be2597-0894-ac9c-6fd9-3a2405c418c2\",\"title\":\"deploy_wizard: SickLeave.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"c994a95e-56a7-9257-3166-3a2405c418c2\",\"title\":\"deploy_wizard: Examination.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"2d5ae25a-a0f9-31cb-4cd4-3a2405c418c2\",\"title\":\"deploy_wizard: PatientRecord.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"ff20b49b-1f58-b8d3-19f4-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardWorkHistory.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"54180c26-839a-5d34-2b27-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardGeneral.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"410fffd5-bbb7-c0ec-0385-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardLab.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"0c82dcd4-14a9-cf98-f90f-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardAnamnesis1.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"31117aaa-61c9-fba6-250d-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardAnamnesis2.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"ef6c4412-3932-97cd-8f6f-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardAnamnesis3.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"d061f282-642d-3118-c6cd-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardResult.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"d4f81759-e8e0-d71e-830e-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCardFile.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"09776b00-042b-4480-39f3-3a2405c418c2\",\"title\":\"deploy_wizard: HealthCard.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"5a6bb1d4-7a53-3017-ed4b-3a240629ff7c\",\"title\":\"create_table: Hr_T_HealthCard\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"34925778-5cbc-4985-5e27-3a240629ff7c\",\"title\":\"create_table: Hr_T_HealthCardWorkHistory\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"5bed6c36-7ed9-239f-f80f-3a240629ff7c\",\"title\":\"create_table: Hr_T_HealthCardFile\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"bdb5b531-c138-f0cb-9651-3a240629ff7c\",\"title\":\"create_table: Hr_T_SickLeave\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"1cd30c82-193c-d4c7-58ab-3a240629ff7c\",\"title\":\"create_table: Hr_T_Examination\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"39c89298-4fdf-72b6-e18c-3a240629ff7c\",\"title\":\"create_table: Hr_T_PatientRecord\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"2f24ba6e-4f9f-693f-3575-3a240629ff7c\",\"title\":\"deploy_wizard: SickLeave.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"476e37df-76c1-d527-5f3c-3a240629ff7c\",\"title\":\"deploy_wizard: Examination.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"49447c0a-286b-dd58-b212-3a240629ff7c\",\"title\":\"deploy_wizard: PatientRecord.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"ce05b42f-606e-2e8e-b940-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardGeneral.json (List, kart olu\\u015Fturma)\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"af296ed5-c6e7-8c38-53c5-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardWorkHistory.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"6a842baa-76b1-674f-f8ab-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardLab.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"ed5d8c8c-4560-6413-d887-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardAnamnesis.json (Anamnez 1-3 birle\\u015Fik)\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"fc9b0b09-181e-e035-3747-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardResult.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"8c7979ad-7851-4d36-de7e-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCardFile.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"1fbb291c-5ff0-9ce0-c896-3a240629ff7c\",\"title\":\"deploy_wizard: HealthCard.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true}]", + "Analysis": "

1. Amaç

\n

İş sağlığı biriminin (işyeri hekimi / İK) İK › Sağlık menüsünde şu kayıtları tutması:

\n
    \n
  • Çalışanların istirahat raporları
  • \n
  • Periyodik ve plansız muayeneler
  • \n
  • Revir hasta kayıtları
  • \n
  • Kişi bazlı sağlık kartı (işe giriş formu)
  • \n
\n

Sağlık Kartları ekranı, Kullanıcı listesine benzer biçimde aktif kullanıcıların tamamını listeler. Detaya geçilince sekmeli kart açılır ve sağlık bilgileri orada girilip düzenlenir. Kart kaydı silinmez.

\n\n

2. Görsel analizi

\n
    \n
  • [image-1] İstirahat Listesi — liste + popup form \"İstirahat Girişi\".\n
      \n
    • Grid: Sicil No, Adı Soyadı, Şube Adı, Departman, Başlangıç / Bitiş Tarihi, Gün, İstirahat Tipi, Kurum, Bölüm, İstirahat Sebebi.
    • \n
    • Form: Adı Soyadı (seçim), Şube Adı, TC Numarası, Departmanı, tarihler, Gün, İstirahat Tipi / Kurum Adı / Bölüm (seçim), İstirahat Sebebi.
    • \n
    • Görünen satır: Kişi ve kurum kayıtlarına bağlı; demo veri üretilmez.
    • \n
  • \n
  • [image-2] Periyodik Muayene Listesi — liste + popup form.\n
      \n
    • Grid: TC, Adı Soyadı, Görevi, Departman, Muayene, Sonuç, Muayene Tarihi, Doktor, Diploma No / Tarihi, İş Yeri Hekim Belge No / Tarihi, Şube Adı.
    • \n
    • Form: Fotoğraf, Adı, T.C Kimlik No, Doğum Tarihi, Tarih (bugün), Eğitim Durumu, Çalıştığı Bölüm, Muayene, Sonuç, Doktor, Diploma Tarihi / No.
    • \n
  • \n
  • [image-3…11] Personel Sağlık Kartı — başlıkta \"İş Yerinin\" ve \"Çalışanın\" alanları, altında şu sekmeler:\n
      \n
    • Genel: önceki iş yerleri, soy / öz geçmiş
    • \n
    • Laboratuvar – Fizik Muayene
    • \n
    • Tıbbi Anamnez 1–3
    • \n
    • Sonuç
    • \n
    • Periyodik Muayeneler
    • \n
    • Hasta Protokol Defteri
    • \n
    • Dosyalar
    • \n
  • \n
  • [image-12] Hasta Kayıt Defteri — liste + popup form.\n
      \n
    • Grid: Sıra No, Adı Soyadı, Adres, Yaş, Cinsiyet, Doktor, Protokol Tipi, İstirahat Günü, Sonuç, Sevk Yeri, Şube Adı, Açıklama.
    • \n
    • Form: Tip varsayılanı Hastalık, Sonuç varsayılanı Tedavi (Taburcu). İstirahat Gün ve Sevk Yeri pasif gelir.
    • \n
  • \n
  • Butonlar: Standart liste ve form butonlarıdır.
  • \n
\n\n

3. Çözüm yolu

\n

Wizard List — tüm ekranlar ListForm'dur; çerçeve dışı değişiklik yoktur.

\n
    \n
  • Sağlık Kartları ana ekranı: Sorgu kaynaklıdır (SelectCommandType: 4).\n
      \n
    • AbpUsers üzerinden aktif kullanıcıları listeler; karttan özet kolonlar LEFT JOIN ile gelir.
    • \n
    • Anahtar kullanıcı Id'sidir.
    • \n
    • Ekleme, güncelleme ve silme kapalı; yalnız detay açıktır.
    • \n
  • \n
  • Detay (/admin/form/App.Hr.HealthCard/{UserId}): 8 sekmeyi alt form olarak açar (Id → UserId).\n
      \n
    • Kart satırını oluşturan sekme: Genel. Bu sekme List tipidir; Yeni ile kart oluşturulur.
    • \n
    • Kartı yalnız güncelleyen sekmeler: Laboratuvar – Fizik, Tıbbi Anamnez, Sonuç. Bu sekmeler Form tipidir.
    • \n
  • \n
\n\n

4. Menü yeri

\n

İnsan Kaynakları › Sağlık › {İstirahat Listesi · Muayene Listesi · Hasta Kayıt Defteri · Sağlık Kartları}

\n\n\n\n\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.Hrİnsan Kaynakları / Human ResourcesFcConferenceCallVar
AraApp.Hr.HealthSağlık / HealthFcLikeYeni
EkranApp.Hr.SickLeaveİstirahat Listesi / Sick Leave ListFcOvertimeYeni
EkranApp.Hr.ExaminationMuayene Listesi / Examination ListFcInspectionYeni
EkranApp.Hr.PatientRecordHasta Kayıt Defteri / Patient RegisterFcViewDetailsYeni
EkranApp.Hr.HealthCardSağlık Kartları / Health CardsFcBusinessContactYeni
\n
    \n
  • Ortak ayarlar: ModuleId Hr · İzin grubu App.Hr (Var) · Route /admin/list/{ListFormCode} · MenuParentPath = [App.Hr], MenuParentCode = App.Hr.Health.
  • \n
  • Yetki: App.Hr › App.Hr.Health › {App.Hr.SickLeave | App.Hr.Examination | App.Hr.PatientRecord | App.Hr.HealthCard}. App.Hr.Health yeni.
  • \n
  • Menüsüz alt ekranlar (6) (CreateMenu: false, aynı dalda): App.Hr.HealthCardGeneral, .HealthCardWorkHistory, .HealthCardLab, .HealthCardAnamnesis, .HealthCardResult, .HealthCardFile.
  • \n
  • Sıra: Alt formu olan ekran, alt formlarından sonra kurulur. Bu yüzden Sağlık Kartları menünün sonundadır.
  • \n
\n\n

5. Veri modeli

\n
    \n
  • Ortak: Tüm tablolar yeni, tenant + tam audit alanlıdır. Ekranlar IsTenant = true, IsBranch = false.
  • \n
  • Tanım tablosu: Hayır (hepsi) — kayıtlar hareket ya da kişiye bağlı kart verisidir; IsActive yoktur.
  • \n
  • Lookup kaynakları: Hr_T_Hospital, Hr_T_HospitalDepartment, Hr_T_Doctor, Hr_T_DocumentType, Hr_T_Department, Hr_T_JobPosition, Sas_T_Branch, AbpUsers (salt okunur).
  • \n
\n

Sağlık Kartları liste sorgusu (SelectCommand, anahtar Id = kullanıcı Id'si):

\n

SELECT u.Id, u.TenantId, u.IsDeleted, u.Name + ' ' + u.Surname AS FullName, u.Email, u.PhoneNumber, d.Name AS DepartmentName, jp.Name AS JobPositionName, hc.IdentityNo, hc.BirthDate, hc.BloodType, ISNULL(hc.ShiftWorkFit, CAST(0 AS bit)) AS ShiftWorkFit, ISNULL(hc.NightWorkFit, CAST(0 AS bit)) AS NightWorkFit, CASE WHEN hc.Id IS NULL THEN CAST(0 AS bit) ELSE CAST(1 AS bit) END AS HasCard FROM AbpUsers u LEFT JOIN Hr_T_HealthCard hc ON hc.UserId = u.Id AND hc.IsDeleted = 'false' LEFT JOIN Hr_T_Department d ON d.Id = u.DepartmentId LEFT JOIN Hr_T_JobPosition jp ON jp.Id = u.JobPositionId WHERE u.IsActive = 'true'

\n

Q-User — ortak personel lookup'ı (İstirahat, Muayene, Hasta Kayıt):

\n

SELECT u.Id AS [Key], u.Name + ' ' + u.Surname AS [Name], hc.IdentityNo, hc.BirthDate, hc.Gender, hc.EducationStatus, hc.HomeAddress, DATEDIFF(YEAR, hc.BirthDate, GETDATE()) AS Age, d.Name AS DepartmentName, jp.Name AS JobPositionName FROM AbpUsers u LEFT JOIN Hr_T_HealthCard hc ON hc.UserId = u.Id AND hc.IsDeleted = 'false' LEFT JOIN Hr_T_Department d ON d.Id = u.DepartmentId LEFT JOIN Hr_T_JobPosition jp ON jp.Id = u.JobPositionId WHERE u.IsDeleted = 'false' AND u.IsActive = 'true' AND u.TenantId = '@TENANTID' ORDER BY u.Name, u.Surname

\n

Q-Doctor — doktor lookup'ı:

\n

SELECT Id AS [Key], FullName AS [Name], DiplomaNo, DiplomaDate, OhsCertificateNo, OhsCertificateDate, ISNULL(CONVERT(varchar(10), DiplomaDate, 104), '') + ' / ' + ISNULL(DiplomaNo, '') AS DiplomaInfo, ISNULL(CONVERT(varchar(10), OhsCertificateDate, 104), '') + ' / ' + ISNULL(OhsCertificateNo, '') AS OhsInfo FROM Hr_T_Doctor WHERE IsDeleted = 'false' AND IsActive = 'true' AND TenantId = '@TENANTID' ORDER BY FullName

\n\n

Hr_T_SickLeave (İstirahat)

\n
    \n
  • UserId (zorunlu — Q-User)
  • \n
  • BranchId (zorunlu — Sas_T_Branch)
  • \n
  • IdentityNo (nvarchar 11), DepartmentName (nvarchar 200) — kopyalanır, salt okunur
  • \n
  • StartDate, EndDate (date, zorunlu)
  • \n
  • DayCount (int, hesaplanır)
  • \n
  • LeaveType (nvarchar 50, zorunlu — Hastalık, İş Kazası, Analık, Meslek Hastalığı)
  • \n
  • HospitalId (Hr_T_Hospital), HospitalDepartmentId (Hr_T_HospitalDepartment)
  • \n
  • Reason (nvarchar MAX)
  • \n
\n\n

Hr_T_Examination (Muayene)

\n
    \n
  • UserId (zorunlu — Q-User), BranchId (zorunlu)
  • \n
  • ExamDate (date, zorunlu)
  • \n
  • ExamType (nvarchar 50, zorunlu, DB varsayılanı 'Periyodik' — Periyodik, Plansız, İşe Giriş)
  • \n
  • Kopyalanan, salt okunur: IdentityNo, BirthDate, EducationStatus, JobPositionName, DepartmentName
  • \n
  • ExamNote, ExamResult (nvarchar MAX)
  • \n
  • DoctorId (zorunlu — Q-Doctor)
  • \n
  • Doktordan kopyalanan, salt okunur: DoctorDiplomaNo, DoctorDiplomaDate, DoctorOhsCertificateNo, DoctorOhsCertificateDate
  • \n
\n\n

Hr_T_PatientRecord (Hasta Kayıt Defteri)

\n
    \n
  • SequenceNo (int)
  • \n
  • UserId (zorunlu — Q-User), BranchId (zorunlu)
  • \n
  • RecordDate (date, zorunlu)
  • \n
  • Kopyalananlar: Age (int), Gender (nvarchar 1), Address (nvarchar MAX)
  • \n
  • ProtocolType (nvarchar 50, zorunlu, DB varsayılanı 'Hastalık' — Hastalık, İş Kazası Dönüş, İstirahat Dönüş)
  • \n
  • DoctorId (Q-Doctor)
  • \n
  • Result (nvarchar 50, zorunlu, DB varsayılanı 'Tedavi (Taburcu)' — Tedavi (Taburcu), Sevk, İstirahat)
  • \n
  • RestDays (int), ReferralPlace (nvarchar 200), Description (nvarchar MAX)
  • \n
\n\n

Hr_T_HealthCard (Sağlık Kartı — kullanıcı başına bir satır)

\n\n\n\n\n\n\n\n
SekmeKolonlar (tip)
KimlikUserId (uniqueidentifier, NOT NULL — ilişkiden gelir)
Genel (List)\n
    \n
  • İş Yerinin: BranchId (formda zorunlu — Ünvanı), SgkWorkplaceNo (nvarchar 50); şubeden kopyalanan WorkplaceAddress, WorkplacePhone, WorkplaceFax, WorkplaceEmail.
  • \n
  • Çalışanın: IdentityNo, BirthPlace, BirthDate, HireDate, Gender, MaritalStatus, EducationStatus, ChildCount, HomeAddress, JobDescription.
  • \n
  • Soy Geçmişi: Mother / Father / Sibling / Child + Status (Sağ, Vefat) ve Note.
  • \n
  • Öz Geçmişi: BloodType, ChronicDisease, Vaccination, Tetanus, Hepatitis, OtherVaccine.
  • \n
Laboratuvar – Fizik (Form)9 laboratuvar alanı (Lab…), 12 fizik muayene alanı (Phys…), BloodPressure, Pulse, Height, Weight, Bmi (decimal; Bmi hesaplanır)
Tıbbi Anamnez (Form, 3 grup)\n
    \n
  • Anamnez 1: 17 bit (Complaint…, Dx…).
  • \n
  • Anamnez 2: Hospitalized, Surgery, WorkAccident, OccDiseaseExam, Disability (bit) + …Note.
  • \n
  • Anamnez 3: UnderTreatment (bit) + Note; Smoking, Alcohol + Note.
  • \n
  • Tüm bit alanları NOT NULL, varsayılan 0.
  • \n
Sonuç (Form)FitnessNote1, FitnessNote2; ResultDoctorId + ResultDoctorDiploma, ResultDoctorOhsCert; ShiftWorkFit, NightWorkFit (bit); ShiftDoctorId + ShiftDoctorDiploma, ShiftDoctorOhsCert
\n\n

Hr_T_HealthCardWorkHistory (Önceki İş Yerleri)

\n
    \n
  • UserId (zorunlu)
  • \n
  • BusinessLine (nvarchar 200, zorunlu)
  • \n
  • JobDone (nvarchar 200)
  • \n
  • StartDate, EndDate (date)
  • \n
\n\n

Hr_T_HealthCardFile (Dosyalar)

\n
    \n
  • UserId (zorunlu)
  • \n
  • FileDate (date, zorunlu)
  • \n
  • DocumentTypeId (Hr_T_DocumentType)
  • \n
  • Description (nvarchar 200)
  • \n
  • FileContent (nvarchar MAX, zorunlu, dxImageUpload)
  • \n
\n\n

6. Etkilenen kısımlar

\n
    \n
  • Ekranlar ve menü: 4 menü ekranı, 6 menüsüz alt ekran, ara menü App.Hr.Health ve ara yetkisi.
  • \n
  • Yetkiler: Her ekranda R + .Create, .Update, .Delete, .Export, .Import, .Note. Alt ekran yetkisi verilmeyen rolde ilgili sekme görünmez.
  • \n
  • Dil anahtarları: Ekran, alan ve widget anahtarları (App.Hr.Health.Widget.*). Var olan App.Listform.ListformField.* metinleri ezilmez.
  • \n
  • Alt form ilişkileri (ana ekrandan, Id → UserId, DbType 9), sırayla:\n
      \n
    • Genel (List)
    • \n
    • Önceki İş Yerleri (List)
    • \n
    • Laboratuvar – Fizik (Form)
    • \n
    • Tıbbi Anamnez (Form)
    • \n
    • Sonuç (Form)
    • \n
    • Periyodik Muayeneler (App.Hr.Examination)
    • \n
    • Hasta Protokol Defteri (App.Hr.PatientRecord)
    • \n
    • Dosyalar
    • \n
  • \n
  • Dokunulmayanlar: AbpUsers ve diğer platform tabloları (yalnız okunur), var olan ekranlar, React sayfaları, iş akışı, bildirim, public yol.
  • \n
\n\n

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

\n
    \n
  • Sağlık Kartları listesi:\n
      \n
    • Tüm aktif kullanıcılar görünür.
    • \n
    • Kolonlar: Adı Soyadı, Departman, Görevi, Telefon, E-posta, TC, Doğum Tarihi, Kan Grubu, Vardiyaya Uygun, Geceye Uygun, Kart Var.
    • \n
    • Yeni / Düzenle / Sil yok; Detay var.
    • \n
    • Görünümler: Grid, Card, Pivot, Chart.
    • \n
    • Widget'lar: Aktif Personel, Kartı Olan, Kartı Olmayan.
    • \n
  • \n
  • Genel sekmesi (List):\n
      \n
    • Grid: Kullanıcının kart satırını gösterir (TC, Doğum Tarihi, Cinsiyet, Ünvanı, Kan Grubu).
    • \n
    • Kartı olmayan kullanıcı: Yeni butonu popup formu açar; kayıt kartı oluşturur, UserId ilişkiden dolar.
    • \n
    • Kartı olan kullanıcı: Satır düzenlenir.
    • \n
    • Silme: Kapalı.
    • \n
    • Popup form grupları: İş Yerinin, Çalışanın, Soy Geçmişi, Öz Geçmişi.
    • \n
  • \n
  • Laboratuvar – Fizik, Tıbbi Anamnez, Sonuç (Form):\n
      \n
    • Yalnız güncelleme yapar; ekleme ve silme kapalıdır.
    • \n
    • Kart Genel sekmesinde oluşturulduktan sonra dolu gelir.
    • \n
    • Tıbbi Anamnez tek sekmede üç grup halindedir.
    • \n
  • \n
  • Önceki İş Yerleri, Dosyalar: Satır ekleme ve silme açıktır. Periyodik Muayeneler, Hasta Protokol Defteri: İlgili menü ekranlarının kişiye süzülmüş hâlidir.
  • \n
  • Kopyalama (EditorScript copy, değişimde):\n
      \n
    • UserId seçilince:\n
        \n
      • İstirahat: IdentityNo, DepartmentName.
      • \n
      • Muayene: IdentityNo, BirthDate, EducationStatus, JobPositionName, DepartmentName.
      • \n
      • Hasta Kayıt: Age, Gender, Address.
      • \n
    • \n
    • Doktor seçilince: Belge bilgileri kopyalanır.
    • \n
    • Genel'de BranchId seçilince: Şube adresi ve iletişim bilgileri kopyalanır.
    • \n
  • \n
  • Hesaplar:\n
      \n
    • DayCount = days(StartDate, EndDate) − 1.
    • \n
    • Bmi = Kilo / (Boy/100)².
    • \n
  • \n
  • Koşullu alanlar (Hasta Kayıt): RestDays yalnız Sonuç = İstirahat, ReferralPlace yalnız Sonuç = Sevk iken açıktır; Sonuç değişince ikisi temizlenir.
  • \n
  • Açılış varsayılanları (alan boşsa): Tarihler bugün; ExamType = Periyodik; ProtocolType = Hastalık; Result = Tedavi (Taburcu).
  • \n
  • Editörler:\n
      \n
    • Evet/Hayır alanları dxSwitch, zorunlu işaretlenmez.
    • \n
    • Seçimler dxSelectBox.
    • \n
    • Uzun metinler dxTextArea.
    • \n
    • FileContent dxImageUpload (görsel + PDF).
    • \n
    • TC 11 haneli maskeli.
    • \n
    • Gizli UserId alanları zorunlu değildir.
    • \n
  • \n
  • Widget'lar:\n
      \n
    • İstirahat: Toplam, Bugün İstirahatte, İş Kazası.
    • \n
    • Muayene: Toplam, Bugün, Periyodik.
    • \n
    • Hasta Kayıt: Toplam, Bugün, Sevk.
    • \n
  • \n
\n\n

8. Kararlar

\n
    \n
  • AbpUsers extension önerisi uygulanmaz.\n
      \n
    • Açıklamada, Form sekmelerinde ekleme yapılamadığı için kart alanlarının AbpUsers'a extension olarak eklenmesi öneriliyor.
    • \n
    • Rehber, platformun hazır tablolarına (AbpUsers) doğrudan yazan ekran üretilmesini yasaklar (reference §12.8); platform tablosuna kolon eklemek de çerçeve dışıdır (§0.8).
    • \n
    • Ayrıca üretim araçları var olan tabloya kolon ekleyemez.
    • \n
    • Aynı ihtiyaç, kart satırının List tipi Genel sekmesinden oluşturulmasıyla karşılanır. Form sekmeleri (Laboratuvar, Anamnez, Sonuç) yalnız bu satırı günceller.
    • \n
  • \n
  • Anamnez: Talep sahibinin önerisiyle Tıbbi Anamnez 1, 2 ve 3 tek sekmede (App.Hr.HealthCardAnamnesis) üç grup olarak birleştirilir. Sekme sayısı 8 olur.
  • \n
  • Önceki İş Yerleri: Genel artık List tipi olduğu için ayrı sekmededir; kullanıcıya bağlıdır ve kart oluşmadan da girilebilir.
  • \n
  • Liste kaynağı: Sağlık Kartları listesi aktif kullanıcılardan beslenir. Kart satırı Genel sekmesinden açılır; otomatik kart açan iş yoktur.
  • \n
  • Silme:\n
      \n
    • Sağlık Kartları listesinde ve kart sekmelerinde (Genel, Laboratuvar, Anamnez, Sonuç) silme yoktur.
    • \n
    • Dosyalar ve Önceki İş Yerleri satırları silinebilir. Muayene ve Hasta Kayıt satırları kendi ekranlarındaki gibi silinebilir (onay).
    • \n
  • \n
  • Tek kart kuralı:\n
      \n
    • Genel sekmesinde kart varken Yeni ile ikinci kart açılmamalıdır. Wizard bunu engelleyecek bir kısıt üretemez.
    • \n
    • Kesin koruma için SQL Query Manager'dan elle eklenecek bir tekil indeks önerilir: CREATE UNIQUE INDEX UX_Hr_T_HealthCard_User ON Hr_T_HealthCard (TenantId, UserId) WHERE IsDeleted = 0 (onay).
    • \n
  • \n
  • Başlık bilgileri:\n
      \n
    • Kullanıcı kaynaklı olanlar (ad, departman, görev, telefon, e-posta) detayda salt okunur gösterilir.
    • \n
    • Karta ait olanlar (TC, doğum, cinsiyet, medeni durum, eğitim, çocuk sayısı, adres, yaptığı iş, iş yeri) Genel sekmesinde düzenlenir (onay).
    • \n
  • \n
  • Fotoğraf: Kullanıcının avatarıdır, Kullanıcılar ekranında yönetilir (onay).
  • \n
  • Kişisel veriler: TC, doğum tarihi, cinsiyet, eğitim ve adres kartta tutulur; diğer ekranlar bunları karttan kopyalar.
  • \n
  • Genel sekmesi alanları:\n
      \n
    • İş Yerinin Ünvanı şube seçimidir; SGK İşyeri Sicil No elle girilir (onay).
    • \n
    • Doğum tarihinin yanındaki ikinci tarih İşe Giriş Tarihi (HireDate) olarak tutulur (onay).
    • \n
  • \n
  • Seçenek listeleri:\n
      \n
    • Soy Geçmişi: Sağ / Vefat (onay).
    • \n
    • Kan Grubu: A, B, AB, 0 × Rh+ / Rh−.
    • \n
    • Sigara: Hayır / Evet / Bıraktı. Alkol: Hayır / Evet / Ara Sıra (onay).
    • \n
    • Eğitim: İlkokul … Doktora.
    • \n
    • Medeni Durum: Bekar, Evli, Dul, Boşanmış.
    • \n
    • Muayene Tipi: Periyodik, Plansız, İşe Giriş (onay).
    • \n
  • \n
  • Sıra No: Hasta Kayıt Defteri'nde elle girilir (onay).
  • \n
  • Kurulum: Ortamda bu talebe ait tablo ve ekran yoktur; plan sıfırdan kurulumdur. Talepteki eski alt görev satırları önceki üretimden kalmıştır.
  • \n
\n\n

9. Üretim planı

\n
    \n
  1. create_table: Hr_T_HealthCard
  2. \n
  3. create_table: Hr_T_HealthCardWorkHistory
  4. \n
  5. create_table: Hr_T_HealthCardFile
  6. \n
  7. create_table: Hr_T_SickLeave
  8. \n
  9. create_table: Hr_T_Examination
  10. \n
  11. create_table: Hr_T_PatientRecord
  12. \n
  13. deploy_wizard: SickLeave.json
  14. \n
  15. deploy_wizard: Examination.json
  16. \n
  17. deploy_wizard: PatientRecord.json
  18. \n
  19. deploy_wizard: HealthCardGeneral.json
  20. \n
  21. deploy_wizard: HealthCardWorkHistory.json
  22. \n
  23. deploy_wizard: HealthCardLab.json
  24. \n
  25. deploy_wizard: HealthCardAnamnesis.json
  26. \n
  27. deploy_wizard: HealthCardResult.json
  28. \n
  29. deploy_wizard: HealthCardFile.json
  30. \n
  31. deploy_wizard: HealthCard.json
  32. \n
\n\n

10. Üretime hazır

\n

Evet

MCP (üretim — List tipi Genel + birleşik Anamnez)

\n
    \n
  • Tablolar (6): Hr_T_HealthCard, Hr_T_HealthCardWorkHistory, Hr_T_HealthCardFile, Hr_T_SickLeave, Hr_T_Examination, Hr_T_PatientRecord. Hepsi tenant ve tam audit alanlı; seed dosyaları host/sql/object/ altında.
  • \n
  • Menü ekranları (İK › Sağlık):\n
      \n
    • App.Hr.SickLeave
    • \n
    • App.Hr.Examination
    • \n
    • App.Hr.PatientRecord
    • \n
    • App.Hr.HealthCard — sorgu kaynaklı, aktif kullanıcılar; Yeni/Düzenle/Sil kapalı, Detay açık.
    • \n
  • \n
  • Detay sekmeleri (8): Hepsi Id → UserId ilişkisiyle bağlı.\n
      \n
    • App.Hr.HealthCardGeneral — List; ekleme ve güncelleme açık, silme kapalı; kart satırı burada oluşur.
    • \n
    • .HealthCardWorkHistory — List.
    • \n
    • .HealthCardLab, .HealthCardAnamnesis (Anamnez 1–3 dört grupta), .HealthCardResult — Form; yalnız güncelleme.
    • \n
    • App.Hr.Examination, App.Hr.PatientRecord, .HealthCardFile — List.
    • \n
  • \n
  • AbpUsers: Değiştirilmedi. Açıklamadaki extension önerisi uygulanmadı; gerekçe analizde (reference §12.8).
  • \n
  • Bekleyen elle adım (öneri): Tek kart kuralı için SQL Query Manager'dan tekil indeks eklenebilir:\n
      \n
    • CREATE UNIQUE INDEX UX_Hr_T_HealthCard_User ON Hr_T_HealthCard (TenantId, UserId) WHERE IsDeleted = 0
    • \n
  • \n
  • Bilinen görünüm notları:\n
      \n
    • Alt ekran gridlerinde UserId kolonu görünür kalabilir; ListForm editöründen gizlenebilir.
    • \n
    • Var olan App.Listform.ListformField.* başlıkları ezilmez.
    • \n
  • \n
  • Demo veri: Yok.
  • \n
  • Alt görev listesi: Önceki üretimden kalan 18 satır da listede duruyor; bu üretimin satırları sona eklendi.
  • \n
  • DB Migrate bekleyen wizard'lar (10): SickLeave, Examination, PatientRecord, HealthCardGeneral, HealthCardWorkHistory, HealthCardLab, HealthCardAnamnesis, HealthCardResult, HealthCardFile, HealthCard.
  • \n
" } ] } diff --git a/configs/seeds/host/data/App.Languages.LanguageText.json b/configs/seeds/host/data/App.Languages.LanguageText.json index e92aef26..54d254c7 100644 --- a/configs/seeds/host/data/App.Languages.LanguageText.json +++ b/configs/seeds/host/data/App.Languages.LanguageText.json @@ -64362,6 +64362,48 @@ "Key": "App.ListForm.WizardDevRequestExport", "Value": "Talebin tüm wizard'larını dışa aktar ({0})" }, + { + "Id": "798fdbec-b481-5961-bfa6-645a9f437cb8", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestDelete", + "Value": "Delete all wizards of this request ({0})" + }, + { + "Id": "b1f17de2-32d4-5bf4-aaea-4fc913776591", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestDelete", + "Value": "Talebin tüm wizard'larını sil ({0})" + }, + { + "Id": "e2ae233f-1d49-510f-a562-5a59d450bb6e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestDeleteConfirm", + "Value": "All wizards of this request are deleted in the order below; sub-forms go before the screen that carries them." + }, + { + "Id": "a8a4e5ca-3e48-57ac-b526-c113bf206a6c", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDevRequestDeleteConfirm", + "Value": "Talebin tüm wizard'ları aşağıdaki sırayla silinir; alt formlar onları taşıyan ekrandan önce gider." + }, + { + "Id": "4bebcab5-7910-57bb-8582-d31c4f5d205e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDeleteOrder", + "Value": "Deletion order" + }, + { + "Id": "3c9dafd9-a74d-5467-870b-e5373fa6d4b5", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.ListForm.WizardDeleteOrder", + "Value": "Silme sırası" + }, { "Id": "0f8facf0-6218-5250-8038-4775b770f7e8", "CultureName": "en", @@ -70451,6 +70493,62 @@ "ResourceName": "Platform", "Key": "App.Listform.ListformField.VisitReason", "Value": "Ziyaret Sebebi" + }, + { + "Id": "90b68f83-f0f5-5fb3-aaef-b62cc24d716e", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.JobPositionName", + "Value": "Position" + }, + { + "Id": "5712290c-c9f4-56e0-a181-0a49eca9d4d6", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.JobPositionName", + "Value": "Görevi" + }, + { + "Id": "7eca5f6d-e78b-5339-ba0d-cfc822f931c6", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.BloodType", + "Value": "Blood Type" + }, + { + "Id": "59b7c58e-3475-5e34-99dd-1c41f6fac0d4", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.BloodType", + "Value": "Kan Grubu" + }, + { + "Id": "88535017-2c1c-54ed-aba5-2ba1c977e8fc", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.ShiftWorkFit", + "Value": "Fit for Shift Work" + }, + { + "Id": "e509bacb-2529-5256-8bde-480d34679839", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.ShiftWorkFit", + "Value": "Vardiyalı Çalışabilir" + }, + { + "Id": "d946cf5c-9dec-5143-92be-7b126520c28d", + "CultureName": "en", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.NightWorkFit", + "Value": "Fit for Night Work" + }, + { + "Id": "96ccf430-d007-52a1-ae5d-378f70724355", + "CultureName": "tr", + "ResourceName": "Platform", + "Key": "App.Listform.ListformField.NightWorkFit", + "Value": "Gece Çalışabilir" } ] } diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index a988290b..a2263f11 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -239,6 +239,19 @@ export interface WizardDeleteAnalysisDto { tables: WizardDeleteTableDto[] } +/** Talebin wizard'ları silinme sırasıyla ve onayda sorulacak tablolar. */ +export interface WizardDeleteDevRequestAnalysisDto { + devRequestId: string + title: string + files: { fileName: string; wizardName: string }[] + tables: WizardDeleteTableDto[] +} + +export interface WizardDeleteDevRequestInput { + devRequestId: string + dropTables: string[] +} + export interface WizardDeleteInput { fileName: string dropTables: string[] diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts index 55c79d8d..903ae3d4 100644 --- a/ui/src/services/wizard.service.ts +++ b/ui/src/services/wizard.service.ts @@ -2,6 +2,8 @@ import { ListFormWizardDto, WizardExportAnalysisDto, WizardDeleteAnalysisDto, + WizardDeleteDevRequestAnalysisDto, + WizardDeleteDevRequestInput, WizardDeleteInput, WizardDeleteResultDto, WizardFileInfoDto, @@ -55,6 +57,22 @@ export const deleteWizardFile = (input: WizardDeleteInput) => data: input, }) +/** Talebin wizard'larinin silinme sirasi ve secilebilen tablolari; hicbir seye dokunmaz. */ +export const getWizardDevRequestDeleteAnalysis = (devRequestId: string) => + apiService.fetchData({ + method: 'GET', + url: `/api/app/list-form-wizard/delete-dev-request-analysis`, + params: { devRequestId }, + }) + +/** Talebin butun wizard'larini bagimlilik sirasiyla siler; rapor tek wizard silmeyle aynidir. */ +export const deleteWizardDevRequest = (input: WizardDeleteDevRequestInput) => + apiService.fetchData({ + method: 'POST', + url: `/api/app/list-form-wizard/delete-dev-request`, + data: input, + }) + /** * Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile * ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir. diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index 3c44619b..72d57896 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -38,6 +38,8 @@ import { exportWizardDevRequest, exportWizardFile, getWizardDeleteAnalysis, + getWizardDevRequestDeleteAnalysis, + deleteWizardDevRequest, getWizardDevRequestExportAnalysis, getWizardExportAnalysis, getWizardFiles, @@ -73,6 +75,10 @@ interface ConfirmState { tables: WizardDeleteTableDto[] | null /** Onayda seçilen, düşürülecek tablolar. */ dropTables: string[] + /** Talebin bütün wizard'ları silinirken dolu; tek wizard silmede yoktur. */ + devRequestId?: string + /** Toplu silmenin sırası; analiz gelene kadar null. */ + order?: { fileName: string; wizardName: string }[] | null } const DELETE_STATUS_ORDER = [ @@ -108,13 +114,21 @@ interface WizardTreeNode { /** * Bağlı wizard'ları üst ekranın altında toplar. Kök, hiçbir wizard'ın bağımlılığı olmayan - * dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Döngüdeki - * dosyaların kökü olmadığı için hiç çizilmeyenler sonda kök olarak eklenir. + * dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Kendi + * menüsü olan ekran başka bir ekranın sekmesinde açılsa da kendi başına bir ekrandır; kök kalır + * (export kapanışı `dependencies` üzerinden onu yine taşır). Döngüdeki dosyaların kökü olmadığı + * için hiç çizilmeyenler sonda kök olarak eklenir. */ const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => { const byName = new Map(files.map((f) => [f.fileName.toLowerCase(), f])) + const isNestable = (f: WizardFileInfoDto) => (f.menuPath ?? []).length === 0 const childNames = new Set( - files.flatMap((f) => (f.dependencies ?? []).map((d) => d.toLowerCase())), + files.flatMap((f) => + (f.dependencies ?? []) + .map((d) => byName.get(d.toLowerCase())) + .filter((c): c is WizardFileInfoDto => !!c && isNestable(c)) + .map((c) => c.fileName.toLowerCase()), + ), ) const rendered = new Set() @@ -124,7 +138,10 @@ const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => { const nextPath = new Set(path).add(key) const children = (file.dependencies ?? []) .map((d) => byName.get(d.toLowerCase())) - .filter((c): c is WizardFileInfoDto => !!c && !nextPath.has(c.fileName.toLowerCase())) + .filter( + (c): c is WizardFileInfoDto => + !!c && isNestable(c) && !nextPath.has(c.fileName.toLowerCase()), + ) .map((c) => build(c, nextPath)) return { file, children } } @@ -155,6 +172,10 @@ interface WizardTreeRow { /** Kökten gelen yol; ortak alt form her üst ekranın altında ayrı açılıp kapanabilsin diye. */ key: string hasChildren: boolean + /** Satır kardeşlerinin sonuncusu mu; bağlantı çizgisi burada biter (└). */ + isLast: boolean + /** Her üst seviye için, o seviyedeki atanın altında kardeş kalıp kalmadığı (│ sürer mi). */ + guides: boolean[] } interface WizardDevRequestGroup { @@ -170,15 +191,32 @@ const flattenWizardTree = ( collapsed: Set, depth = 0, prefix = '', + guides: boolean[] = [], ): WizardTreeRow[] => - nodes.flatMap((node) => { + nodes.flatMap((node, index) => { const key = `${prefix}${node.file.fileName}` - const row = { file: node.file, depth, key, hasChildren: node.children.length > 0 } + const isLast = index === nodes.length - 1 + const row = { + file: node.file, + depth, + key, + hasChildren: node.children.length > 0, + isLast, + guides, + } + // Kök satırın altından çizgi inmez; çocuklar kendi kolunu kökün aç/kapa düğmesinden çizer. + const childGuides = depth === 0 ? [] : [...guides, !isLast] return collapsed.has(key) ? [row] - : [row, ...flattenWizardTree(node.children, collapsed, depth + 1, `${key}/`)] + : [row, ...flattenWizardTree(node.children, collapsed, depth + 1, `${key}/`, childGuides)] }) +/** Bir ağaç seviyesinin genişliği: aç/kapa düğmesi (14px) + satırdaki boşluk (8px). */ +const TREE_INDENT = 22 +/** Dikey çizginin seviye içindeki yeri; aç/kapa düğmesinin ortasına denk gelir. */ +const TREE_LINE_OFFSET = 15 +const TREE_LINE = 'absolute border-gray-300 dark:border-gray-600' + const WizardFileManager = () => { const navigate = useNavigate() const { translate } = useLocalization() @@ -400,6 +438,33 @@ const WizardFileManager = () => { } } + /** + * Talebin bütün wizard'larının silme onayı. Sıra ve tablolar sunucudan gelir: tablo kullanımı + * talebin kendi ekranlarını saymaz, hepsi aynı işlemde silinir. + */ + const openDevRequestDeleteConfirm = async (f: WizardFileInfoDto) => { + const devRequestId = f.devRequestId! + const key = `devRequest:${devRequestId}` + setConfirm({ + fileName: key, + wizardName: f.devRequestTitle || devRequestId, + tables: null, + dropTables: [], + devRequestId, + order: null, + }) + try { + const res = await getWizardDevRequestDeleteAnalysis(devRequestId) + setConfirm((prev) => + prev?.fileName === key + ? { ...prev, tables: res.data.tables ?? [], order: res.data.files ?? [] } + : prev, + ) + } catch { + setConfirm((prev) => (prev?.fileName === key ? { ...prev, tables: [], order: [] } : prev)) + } + } + const toggleDropTable = (tableName: string, checked: boolean) => setConfirm((prev) => prev @@ -417,10 +482,15 @@ const WizardFileManager = () => { setDeletingFile(confirm.fileName) setConfirm(null) try { - const res = await deleteWizardFile({ - fileName: confirm.fileName, - dropTables: confirm.dropTables, - }) + const res = confirm.devRequestId + ? await deleteWizardDevRequest({ + devRequestId: confirm.devRequestId, + dropTables: confirm.dropTables, + }) + : await deleteWizardFile({ + fileName: confirm.fileName, + dropTables: confirm.dropTables, + }) setDeleteReport(res.data) toast.push( @@ -682,6 +752,20 @@ const WizardFileManager = () => { onClick={() => handleExport(job)} /> )} + {canDelete && ( +