From 2d48626219cf68e3b1fa2b54c616089bd2f4725f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?=
<76204082+iamsedatozturk@users.noreply.github.com>
Date: Sat, 10 Oct 2026 23:11:12 +0300
Subject: [PATCH] =?UTF-8?q?Sat=C4=B1nalma=20Talepleri=20d=C3=BCzenlemeleri?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../lowcode-reference.instructions.md | 1 +
.github/instructions/lowcode.instructions.md | 66 +-
README.md | 5 +-
.../GridOptionsDto/GridEditingDto.cs | 4 +
.../ListForms/Wizard/ListFormWizardDto.cs | 5 +-
.../ListForms/Wizard/WizardSeedFileDto.cs | 2 +-
...CreateUpdateListFormWorkflowCriteriaDto.cs | 4 +
.../ListForms/Workflow/DecisionWorkflowDto.cs | 3 +
.../Workflow/IListFormWorkflowAppService.cs | 4 +-
.../Workflow/ListFormWorkflowCriteriaDto.cs | 4 +
.../Workflow/WorkflowRejectReasonDto.cs | 51 +
.../Workflow/WorkflowRejectReasonItemDto.cs | 7 +
.../ListForms/ListFormSelectAppService.cs | 147 +-
.../ListForms/ListFormWorkflowAppService.cs | 323 +-
.../ListFormWorkflowCriteriaMapper.cs | 125 +
.../Migrations/WizardDataSeeder.cs | 1 +
.../WizardConsts.cs | 6 +-
.../Tenant/ListForm/ListFormWorkflow.cs | 6 +
.../EntityFrameworkCore/PlatformDbContext.cs | 1 +
.../20261009171249_Initial.Designer.cs | 3 +
.../Migrations/20261009171249_Initial.cs | 3 +-
.../PlatformDbContextModelSnapshot.cs | 3 +
claude.md | 9 +-
.../seeds/data/App.Definitions.Approver.json | 11 +-
.../53294e77-a4fb-45e4-a80e-acf07b5d75be.json | 6 +-
.../fc6d0fbb-6b04-4d15-9d27-fec98e5c30fb.json | 2 +-
configs/seeds/data/App.Hr.Department.json | 6 +
.../data/App.Languages.LanguageText.json | 448 +--
.../seeds/docs/modules/App.Administration.md | 6 +-
configs/seeds/docs/modules/App.Prc.md | 154 +-
.../object/Prc_P_MaterialRequestAfterSave.sql | 56 +-
.../postgres/object/Prc_T_MaterialRequest.sql | 16 +-
.../object/Prc_V_MaterialRequestLine.sql | 11 +
.../object/Prc_P_MaterialRequestAfterSave.sql | 66 +-
.../sql/object/Prc_T_MaterialRequest.sql | 18 +-
.../sql/object/Prc_V_MaterialRequestLine.sql | 12 +
configs/seeds/wizard/Adm_Approver.json | 14 +-
configs/seeds/wizard/Prc_MaterialRequest.json | 2628 ++++++++---------
.../Prc_MaterialRequestDeptApproval.json | 527 ++--
.../seeds/wizard/Prc_MaterialRequestLine.json | 19 +-
.../Prc_MaterialRequestPurchaseApproval.json | 1174 ++------
.../seeds/wizard/Prc_MyMaterialRequest.json | 1480 ++--------
configs/seeds/wizard/Prc_RejectReason.json | 4 +-
ui/src/proxy/admin/wizard/models.ts | 1 +
ui/src/proxy/form/models.ts | 11 +
ui/src/routes/route.constant.ts | 6 +
ui/src/services/workflow.service.ts | 22 +-
ui/src/utils/workflow/workflowHelpers.ts | 17 +
.../edit/FormTabDatabaseDataSource.tsx | 171 +-
.../views/admin/listForm/edit/FormTabEdit.tsx | 617 +---
.../admin/listForm/edit/FormTabWorkflow.tsx | 1 +
ui/src/views/admin/listForm/wizard/Wizard.tsx | 3 +
.../wizard/WizardStepDataSettings.tsx | 13 +
.../listForm/wizard/WizardStepWorkflow.tsx | 3 +
.../listForm/workflow/WorkflowCriteria.tsx | 156 +-
.../listForm/workflow/WorkflowDesigner.tsx | 3 +
.../developerKit/SqlTableDesignerDialog.tsx | 8 +-
ui/src/views/form/FormButtons.tsx | 14 +-
ui/src/views/form/useFormData.tsx | 14 +-
ui/src/views/list/CardView.tsx | 12 +-
ui/src/views/list/Grid.tsx | 11 +-
ui/src/views/list/SchedulerView.tsx | 7 +-
ui/src/views/list/Tree.tsx | 11 +-
ui/src/views/list/shared/editingForm.ts | 50 +
ui/src/views/list/shared/hooks.ts | 25 +
.../list/shared/templateEditorValidation.tsx | 29 +
ui/src/views/list/useListFormColumns.ts | 14 +-
ui/src/views/list/useToolbar.tsx | 89 +-
68 files changed, 3476 insertions(+), 5273 deletions(-)
create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonDto.cs
create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonItemDto.cs
create mode 100644 api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowCriteriaMapper.cs
create mode 100644 configs/seeds/postgres/object/Prc_V_MaterialRequestLine.sql
create mode 100644 configs/seeds/sql/object/Prc_V_MaterialRequestLine.sql
create mode 100644 ui/src/views/list/shared/templateEditorValidation.tsx
diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md
index 2683b2fc..1289638a 100644
--- a/.github/instructions/lowcode-reference.instructions.md
+++ b/.github/instructions/lowcode-reference.instructions.md
@@ -272,6 +272,7 @@ metnini biçimlendirmek için — sözlükte tipi yoktur, olduğu gibi bileşene
| `AllowAllDeleting` | `false` | Toplu silme düğmesi |
| `AllowDuplicate` | `false` | Kaydı kopyalayarak yeni kayıt |
| `AllowDetail` | `false` | Satırda Detay düğmesi |
+| `OpenDetailAfterInsert` | `false` | Popup'tan eklenen kayıt kaydedilince aynı sekmede kaydın form sayfasına (`/admin/form/{ListFormCode}/{Id}`) geçilir. Grid, Tree, Card ve Scheduler'da çalışır; anahtar insert yanıtından alınır, dönmezse geçilmez. Wizard cevabında da aynı adla vardır (`ListFormWizardDto.OpenDetailAfterInsert`); ne zaman açılacağı `lowcode` §4.2 Davranış tablosunda |
| `ConfirmDelete` | `true` | Silme onayı |
| `UseIcons` | `false` | Aksiyonlar ikon olarak |
| `NewRowPosition` | `"viewportTop"` | `first` \| `last` \| `pageTop` \| `pageBottom` \| `viewportTop` \| `viewportBottom` |
diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md
index 07cff7fe..ad8c5e71 100644
--- a/.github/instructions/lowcode.instructions.md
+++ b/.github/instructions/lowcode.instructions.md
@@ -816,7 +816,8 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma.
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.
+ (§4.3). Ayrıntı: §4.6. Sekmede satır girilen ana ekran için `OpenDetailAfterInsert` kararı
+ (kaydedince Detay'a geçiş) burada yazılır — başlık + kalem ekranında varsayılan `true`.
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.
@@ -1297,6 +1298,7 @@ atanmaz (admin dahil); ekranı görecek roller yetkiyi rol ekranından alır.
| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` üretilir; migrate dosyadaki satırları yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. |
| `SeedSync` | `[ListForm]` `SeedData` açıkken `SeedSyncInsert/Update/Delete`'i açar: ekrandan eklenen/değiştirilen/silinen kayıt dosyaya yansır. **Varsayılan `false`**; demo veri bunu açmaz, yalnızca istekte açıkça istenirse `true` yazılır. Okurken üç bayraktan biri açıksa `true`; tek tek ayar `ListForm` bölümünde yapılır. |
| `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. **`SubForms` doluysa cevaptan bağımsız `true` üretilir** (`ListFormWizardDto.BuildListForm`): wizard alt formları `Form` tipinde kurar ve sekmeler yalnızca Detay görünümünde açılır. Elle yazılan wizard dosyasında da alt formu olan ekranın `EditingOptionDto.AllowDetail` değeri `true` yazılır. **Alt formu olmayan listede `false` kalır** — MCP/AI üretiminde Detay düğmesi yalnızca `SubForms` taşıyan listeye açılır; kullanıcı açıkça istemedikçe tek tablolu liste ya da alt form ekranının kendisi (`PerformancePeriodSkill` gibi) Detay almaz. |
+| `OpenDetailAfterInsert` | `[ListForm]` Popup'tan **yeni** kayıt kaydedilince listede kalınmaz, aynı sekmede kaydın Detay formuna (`/admin/form/{ListFormCode}/{anahtar}`) geçilir (`EditingOptionDto.OpenDetailAfterInsert`; Grid, Tree, Card, Scheduler). Varsayılan `false`. **Analizde karar verilir:** alt form sekmesinde satır girilen başlık + kalem ekranında (talep + talep satırları, sipariş + sipariş kalemleri, masraf + masraf kalemleri) `true` yazılır — kalemler yalnız Detay'da girildiği için kullanıcı başlığı kaydedip doğrudan kalem girişine geçer; kullanıcı listede kalmak istediğini söylerse `false`. Alt formu olmayan listede ve alt form ekranının kendisinde `false`. `AllowDetail` ile birlikte anlamlıdır; `KeyFieldName` ve insert'in anahtarı döndürmesi şarttır (varsayılan insert döndürür). |
| `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). **Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` varsayılan `true`**, diğer dördü `false` (§9.8). Açtığın her görünümün option bloğunu da doldur. |
@@ -1751,6 +1753,12 @@ ekrandan açılır.
Alan başına DevExtreme doğrulama kuralları dizisi. Wizard `IsRequired: true` alanlara `required`
kuralını kendi yazar; gerisi elle eklenir.
+Grid/Tree'de kendi şablonuyla çizilen editörlere (lookup'lı `dxTagBox`/`dxGridBox`, `dxImageUpload`,
+`dxRating`) DevExtreme kolon kuralı uygulamaz (E1050); bu alanlara kolon kuralı verilmez, `required`
+kuralı kayıttan önce istemcide denetlenir (`getTemplateEditorRequiredErrors`) ve boşsa kayıt
+iptal edilip popup açık kalır. Bu editörlerde yalnızca `required` çalışır; diğer kural tipleri
+gerekiyorsa sunucu tarafı kontrol yazılır.
+
| `Type` | Kullanılan alanlar |
| --- | --- |
| `required` | `Message`, `Trim` |
@@ -1830,7 +1838,7 @@ cevaba yazılmaz.
"MenuUrl": "/admin/list/App.Mrp.Orders",
"IsTenant": true, "IsBranch": false, "IsOrganizationUnit": false,
"AllowAdding": true, "AllowUpdating": true, "AllowDeleting": true,
- "AllowDetail": false, "ConfirmDelete": true,
+ "AllowDetail": false, "OpenDetailAfterInsert": false, "ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true, "Card": true, "Pivot": true, "Chart": true,
"Tree": false, "Gantt": false, "Scheduler": false, "Todo": false,
@@ -2281,7 +2289,26 @@ wizard'ınkinden **küçük** olmalıdır (§0.2).
### 4.7 İş akışı (onay süreci)
-`WorkflowDto` onay alanlarını tabloda karşılığı olan kolonlara bağlar:
+`WorkflowDto` onay alanlarını tabloda karşılığı olan kolonlara bağlar. **İş akışlı her tablo** tablo
+tasarımcısının Workflow şablonundaki (`WORKFLOW_COLUMNS`, `SqlTableDesignerDialog`) dört kolonu taşır ve
+bu dördü dışında akış kolonu açılmaz:
+
+| Kolon | SQL Server | PostgreSQL | `WorkflowDto` alanı |
+| --- | --- | --- | --- |
+| `ApprovalUserName` | `nvarchar(256) NULL` | `varchar(256) NULL` | `ApprovalUserFieldName` — sıradaki onaycı(lar)ın kullanıcı adı, `\|` ayrılmış |
+| `ApprovalStatus` | `nvarchar(50) NULL` | `varchar(50) NULL` | `ApprovalStatusFieldName` — bulunulan düğümün başlığı; boş = akış başlamadı (taslak) |
+| `ApprovalDate` | `datetime NULL` | `timestamp NULL` | `ApprovalDateFieldName` |
+| `ApprovalDescription` | `nvarchar(200) NULL` | `varchar(200) NULL` | `ApprovalDescriptionFieldName` — karar açıklaması; reddin nedeni de buraya yazılır |
+
+- Ayrı bir durum kolonu (`Status` vb.) açılmaz; kaydın durumu `ApprovalStatus`'tür ve yalnız akış yazar.
+ Widget/filtre sorguları düğüm başlığıyla süzer (`"ApprovalStatus" = N'Onaylandı'`).
+- Onaylayan Guid kolonu (`ApproverId`) açılmaz; motor kullanıcı adı yazar.
+- Red nedeni kolonu açılmaz: onay düğümünün `RejectReason` tanımı (aşağıda) nedeni listeden sorar ve
+ `ApprovalDescription`'a yazar.
+- Dört alan popup düzenleme formuna konmaz (`EditGroupOrderNo`/`EditOrderNo` boş, ayrı "Onay" grubu
+ açılmaz); listede kolon olarak görünür (`Visible: true`).
+
+| Alan | Tabloda karşılığı |
| Alan | Tabloda karşılığı |
| --- | --- |
@@ -2290,7 +2317,7 @@ wizard'ınkinden **küçük** olmalıdır (§0.2).
| `ApprovalStatusFieldName` | Durum |
| `ApprovalDescriptionFieldName` | Onay/ret açıklaması |
| `ApprovalIsFilterUserName` | Liste yalnızca kullanıcının onayına düşenleri göstersin |
-| `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün |
+| `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün. `ApprovalIsManualStart` açıksa sıfırlama akışı yeniden çalıştırmaz; kayıt taslağa döner ve akış başlat düğmesiyle başlar |
| `ApprovalIsManualStart` | Yeni kayıt akışı kendiliğinden başlatmasın; kayıt taslak kalır, akış toolbar'daki başlat düğmesiyle başlar. Onaya gönderilmiş başlığa satır eklemeyi engelleyen alt formu olan ekranlarda (talep + satırlar) açılır; kapalıyken Grid/Tree yeni kaydın ardından akışı başlatır |
`Criteria` düğüm grafiğidir. `Kind` değerleri: **`Start`**, **`Compare`**, **`Approval`**,
@@ -2298,10 +2325,10 @@ wizard'ınkinden **küçük** olmalıdır (§0.2).
```jsonc
"WorkflowDto": {
- "ApprovalUserFieldName": "ApproverId",
+ "ApprovalUserFieldName": "ApprovalUserName",
"ApprovalDateFieldName": "ApprovalDate",
- "ApprovalStatusFieldName": "Status",
- "ApprovalDescriptionFieldName": "ApprovalNote",
+ "ApprovalStatusFieldName": "ApprovalStatus",
+ "ApprovalDescriptionFieldName": "ApprovalDescription",
"ApprovalIsFilterUserName": true,
"ApprovalIsResetWorkflow": true,
"Criteria": [
@@ -2348,6 +2375,13 @@ Kurallar:
adımı, `RequiresManagerApproval`, `App.Hr.md` › İzin Yönetimi §5.6).
- `Approval` düğümü `NextOnApprove` / `NextOnReject`, `Start` ve `Inform` düğümleri
`NextOnStart` alanını kullanır. `End` düğümünün çıkışı yoktur.
+- **Red nedeni listesi:** `Approval` düğümü opsiyonel `RejectReason { TableName, ValueField, TextField,
+ TargetField, Required }` taşır (entity'de `RejectReasonJson`). Boşsa red yalnızca açıklama ister; doluysa
+ karar diyaloğu tablonun Id/metin kolonlarından seçim listeler (`reject-reasons` ucu; tablo ekranın veri
+ kaynağındadır, adlar katalogdan çözülür, `TenantId`/`IsDeleted`/`IsActive` kolonu varsa süzülür).
+ `Required` açıkken neden seçilmeden ret verilemez. Seçilen nedenin metni açıklamanın başına
+ (`ApprovalDescriptionFieldName`) ve iş akışı notuna, Id'si `TargetField` doluysa kaydın o kolonuna yazılır;
+ metin istemciden alınmaz, sunucu tablodan doğrular. Sıfırlama `TargetField` kolonlarını da temizler.
- **Kayda göre değişen onaycı:** `Approver` değeri `@Kolon` ise (ör. `@ManagerUserNames`) onaycı kaydın o
kolonundan okunur (`ListFormWorkflowAppService.ResolveNodeApprover`); kolon `|` ile ayrılmış kullanıcı
adı/e-posta taşır ve ekranın alanı olmalıdır (satır ListForm sorgusundan okunur; gizli alan yeter). Kolon
@@ -2580,7 +2614,7 @@ bölümünü doldurur.** Keşif turu (§0.6) soruları da bu adımlarla bire bir
| 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) |
| 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) |
| 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode` (ekranda eksik seviyeler menü diyaloğuyla kurulur; MCP `MenuParentPath` yazar), `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` |
-| 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` |
+| 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `OpenDetailAfterInsert`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` |
| 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) |
| 5 | Alt Formlar | yalnız List | `SubForms[]` |
| 6 | Widget'lar | yalnız List | `Widgets[]` |
@@ -4970,18 +5004,18 @@ isteniyorsa geçilir.
**Adım adım:**
-1. **Tablo** — iş kolonları + §8.1 varsayılanları. Onay akışı için dört kolon şarttır:
- durum (`Status`), onaylayan (`ApproverId`), onay tarihi (`ApprovalDate`), açıklama
- (`ApprovalNote`). Talep sahibi için de bir kolon (`EmployeeId`) bulunmalıdır.
+1. **Tablo** — iş kolonları + §8.1 varsayılanları + §4.7'deki dört iş akışı kolonu
+ (`ApprovalUserName`, `ApprovalStatus`, `ApprovalDate`, `ApprovalDescription`; ayrı `Status` yok).
+ Talep sahibi için de bir kolon (`EmployeeId`) bulunmalıdır.
2. **Wizard dosyası** — §4.4'teki iskelet:
- `IsTenant: true` (tabloda `TenantId` var), `IsDeletedField: true`, `IsCreatedField: true`
- `MenuParentCode` = modül kökü (`App.Hr`); yoksa wizard yaratır, `Order = max + 1`
- `PermissionGroupName` = `App.Hr`, EN + TR görünen adlarıyla
- Görünümler: varsayılan dörtlü (`Grid` + `Card` + `Pivot` + `Chart`, `DefaultLayout: "grid"`);
durum akışı görsel isteniyorsa ek olarak `Todo: true` + `TodoOptionDto` (§9.8)
-3. **Alanlar** — `Groups` içinde iki grup mantıklıdır: "Talep" (tutar, tarih, açıklama) ve
- "Onay" (durum, onaylayan, onay tarihi, onay notu). Onay grubundaki alanlar
- `EditorOptions: "{\"readOnly\":true}"` ile kilitlenir; kullanıcı doldurmaz, akış doldurur.
+3. **Alanlar** — düzenleme formunda yalnız "Talep" grubu (tutar, tarih, açıklama) bulunur. İş akışı
+ alanları (onay durumu, onaylayan, onay tarihi, onay açıklaması) forma konmaz, listede görünür;
+ akış doldurur (§4.7).
4. **Hesap ve varsayılanlar** — `EditorScript` ile:
- Talep tarihi form açılışında bugün: `today` tarifi, tetikleyici `open`
- Tutar × oran gibi hesaplar: `multiply` / `percent`
@@ -5000,7 +5034,8 @@ isteniyorsa geçilir.
**Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi
(alt form olarak masraf kalemleri → menüsüz wizard + `SubForms`), satın alma talebi (kalemler +
-tedarikçi lookup'ı + iki kademeli onay).
+tedarikçi lookup'ı + iki kademeli onay). Kalem taşıyan türevlerde ana ekran `AllowDetail: true` +
+`OpenDetailAfterInsert: true` ile gelir: başlık kaydedilince kalem girişi için Detay açılır.
---
@@ -5039,6 +5074,7 @@ Prompt ile üretim yaptıysan ek olarak:
- [ ] 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).
- [ ] `AllowDetail: true` yalnızca `SubForms` taşıyan listede; alt formu olmayan liste ve alt form ekranının kendisi `false`.
+- [ ] Başlık + kalem ekranında (kalemler alt form sekmesinde giriliyor) `OpenDetailAfterInsert: true`; kararı analiz yazdı (§4.2 Davranış).
- [ ] 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
diff --git a/README.md b/README.md
index bb8341c7..419dc17e 100644
--- a/README.md
+++ b/README.md
@@ -522,8 +522,9 @@ EditorScript = EditorScript.Build(
`FormTabWorkflow` ve `views/admin/listForm/workflow/WorkflowDesigner.tsx` üzerinden görsel
tasarlanır. Düğüm türleri koşul karşılaştırması (`CompareColumn`/`CompareOperator`/`CompareValue`)
ve onay adımlarıdır; her adım için `NextOnTrue`, `NextOnFalse`, `NextOnApprove`, `NextOnReject`
-geçişleri tanımlanır. Onaycı birden fazla kişi olabilir. Formlar içinde iş akışına bağlı özel
-butonlar gösterilebilir.
+geçişleri tanımlanır. Onaycı birden fazla kişi olabilir. Onay düğümü reddi yalnızca açıklamayla ya da
+bir tablodan seçilen nedenle (Id + metin kolonu) aldırabilir (`lowcode.instructions.md` §4 › Red nedeni
+listesi). Formlar içinde iş akışına bağlı özel butonlar gösterilebilir.
### 7.6. Yeni ekran ekleme akışı (Wizard)
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingDto.cs
index 44a0fd84..de4d242b 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridEditingDto.cs
@@ -19,6 +19,10 @@ public class GridEditingDto
public bool AllowAllDeleting { get; set; } = false;
public bool AllowAdding { get; set; } = false;
public bool AllowDuplicate { get; set; } = false;
+ ///
+ /// Popup'tan eklenen kayıt kaydedilince listede kalmak yerine kaydın detay formuna (form sayfası) geçilir.
+ ///
+ public bool OpenDetailAfterInsert { get; set; } = false;
public bool UseIcons { get; set; } = false;
public bool ConfirmDelete { get; set; } = true;
/// Accepted Values: 'first' | 'last' | 'pageBottom' | 'pageTop' | 'viewportBottom' | 'viewportTop'
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 bde9004d..98fc8c42 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs
@@ -97,6 +97,8 @@ public class ListFormWizardDto
public bool AllowUpdating { get; set; }
public bool AllowDeleting { get; set; }
public bool AllowDetail { get; set; }
+ /// Popup'tan eklenen kayit kaydedilince detay formuna gecilir (GridEditingDto.OpenDetailAfterInsert).
+ public bool OpenDetailAfterInsert { get; set; }
public bool ConfirmDelete { get; set; }
///
@@ -341,7 +343,7 @@ public class ListFormWizardDto
dto.EditingOptionJson = WizardConsts.DefaultEditingOptionJson(
titleLangKey, formWidth, formHeight,
AllowDeleting, AllowAdding, AllowUpdating,
- ConfirmDelete, false, AllowDetail || subForms.Count > 0);
+ ConfirmDelete, false, AllowDetail || subForms.Count > 0, OpenDetailAfterInsert);
dto.DeleteFieldsDefaultValueJson = isDeletedField
? WizardConsts.DefaultDeleteFieldsDefaultValueJson(KeyFieldDbSourceType)
: WizardConsts.DefaultDeleteFieldsJsonOnlyId(KeyFieldDbSourceType);
@@ -498,6 +500,7 @@ public class ListFormWizardDto
AllowUpdating = editing.AllowUpdating;
AllowDeleting = editing.AllowDeleting;
AllowDetail = editing.AllowDetail;
+ OpenDetailAfterInsert = editing.OpenDetailAfterInsert;
ConfirmDelete = editing.ConfirmDelete;
TreeOptionDto = listForm.TreeOptionDto;
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 3a24f147..58fcfe81 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs
@@ -84,7 +84,7 @@ public class WizardSeedFileDto
[
nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit),
nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting),
- nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData), nameof(ListFormWizardDto.SeedSync),
+ nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.OpenDetailAfterInsert), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData), nameof(ListFormWizardDto.SeedSync),
nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card),
nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart),
nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo),
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs
index b34701c7..02940495 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Text.Json.Serialization;
namespace Sozsoft.Platform.ListForms;
@@ -20,5 +21,8 @@ public class CreateUpdateListFormWorkflowCriteriaDto
public int PositionX { get; set; }
public int PositionY { get; set; }
public List CompareOutcomes { get; set; } = [];
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WorkflowRejectReasonDto RejectReason { get; set; }
}
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs
index f0b7bb47..6d2919a4 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs
@@ -7,5 +7,8 @@ public class DecisionWorkflowDto
public string CurrentNodeId { get; set; }
public bool Approved { get; set; }
public string Note { get; set; }
+
+ /// Reddin listeden seçilen nedeni; düğümün değeri.
+ public string RejectReasonId { get; set; }
}
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs
index 5d168c53..db5fd8fe 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.ListForms;
@@ -12,4 +13,5 @@ public interface IListFormWorkflowAppService : IApplicationService
Task StartAsync(StartWorkflowDto input);
Task ResetAsync(StartWorkflowDto input);
Task DecisionAsync(DecisionWorkflowDto input);
+ Task> GetRejectReasonsAsync(string listFormCode, string criteriaId);
}
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs
index ff7a1550..c726fcba 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ListForms;
@@ -20,5 +21,8 @@ public class ListFormWorkflowCriteriaDto : EntityDto
public int PositionX { get; set; }
public int PositionY { get; set; }
public List CompareOutcomes { get; set; } = [];
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WorkflowRejectReasonDto RejectReason { get; set; }
}
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonDto.cs
new file mode 100644
index 00000000..130e56be
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonDto.cs
@@ -0,0 +1,51 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Sozsoft.Platform.ListForms;
+
+///
+/// Onay düğümünde reddin bir listeden seçilen nedenle verilmesi. Tablo tanımlı değilse red yalnızca
+/// açıklama ister; tanımlıysa karar diyaloğu tablonun /
+/// kolonlarından seçenek listeler.
+///
+public class WorkflowRejectReasonDto
+{
+ public string TableName { get; set; }
+ public string ValueField { get; set; }
+ public string TextField { get; set; }
+
+ /// Seçilen nedenin değerinin yazılacağı kayıt kolonu; boşsa yalnızca not ve açıklamaya yazılır.
+ public string TargetField { get; set; }
+
+ public bool Required { get; set; } = true;
+
+ [JsonIgnore]
+ public bool IsConfigured =>
+ !string.IsNullOrWhiteSpace(TableName) &&
+ !string.IsNullOrWhiteSpace(ValueField) &&
+ !string.IsNullOrWhiteSpace(TextField);
+
+ /// Entity kolonu ile DTO arasındaki tek dönüşüm; yapılandırılmamış tanım null saklanır.
+ public static string Serialize(WorkflowRejectReasonDto value)
+ {
+ return value?.IsConfigured == true ? JsonSerializer.Serialize(value) : null;
+ }
+
+ public static WorkflowRejectReasonDto Deserialize(string json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return null;
+ }
+
+ try
+ {
+ var value = JsonSerializer.Deserialize(json);
+ return value?.IsConfigured == true ? value : null;
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+}
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonItemDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonItemDto.cs
new file mode 100644
index 00000000..01b69d16
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowRejectReasonItemDto.cs
@@ -0,0 +1,7 @@
+namespace Sozsoft.Platform.ListForms;
+
+public class WorkflowRejectReasonItemDto
+{
+ public string Id { get; set; }
+ public string Text { get; set; }
+}
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs
index fd2cca84..7e3398bb 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs
@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
+using Sozsoft.Platform.ListForms.Workflow;
using Sozsoft.Platform.Queries;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Users;
@@ -486,152 +487,10 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
return await sequenceManager.GetNextSequenceAsync(SequenceName);
}
- //Workflow kriterlerini alma
private async Task> GetWorkflowCriteriaAsync(string listFormCode)
{
- var criteria = await listFormWorkflowRepository.GetListAsync(x => x.ListFormCode == listFormCode);
- return OrderWorkflowCriteria(criteria.Select(MapWorkflowCriteria).ToList());
- }
-
- private static List OrderWorkflowCriteria(List criteria)
- {
- if (criteria.Count <= 1)
- {
- return criteria;
- }
-
- var criteriaById = criteria.ToDictionary(x => x.Id);
- var ordered = new List();
- var visited = new HashSet();
- var visiting = new HashSet();
-
- var roots = criteria
- .Where(x => x.Kind == "Start")
- .OrderBy(x => x.Id)
- .ToList();
-
- if (roots.Count == 0)
- {
- var referencedIds = criteria
- .SelectMany(GetWorkflowTargetIds)
- .Where(criteriaById.ContainsKey)
- .ToHashSet();
-
- roots = criteria
- .Where(x => !referencedIds.Contains(x.Id))
- .OrderBy(x => x.Id)
- .ToList();
- }
-
- foreach (var root in roots)
- {
- VisitWorkflowCriteria(root);
- }
-
- foreach (var item in criteria.OrderBy(x => x.Id))
- {
- VisitWorkflowCriteria(item);
- }
-
- return ordered;
-
- void VisitWorkflowCriteria(ListFormWorkflowCriteriaDto item)
- {
- if (visited.Contains(item.Id) || visiting.Contains(item.Id))
- {
- return;
- }
-
- visiting.Add(item.Id);
- ordered.Add(item);
-
- foreach (var targetId in GetWorkflowTargetIds(item))
- {
- if (criteriaById.TryGetValue(targetId, out var target))
- {
- VisitWorkflowCriteria(target);
- }
- }
-
- visiting.Remove(item.Id);
- visited.Add(item.Id);
- }
- }
-
- private static IEnumerable GetWorkflowTargetIds(ListFormWorkflowCriteriaDto criteria)
- {
- if (!criteria.NextOnStart.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnStart;
- }
-
- foreach (var outcome in criteria.CompareOutcomes ?? [])
- {
- if (!outcome.TargetId.IsNullOrWhiteSpace())
- {
- yield return outcome.TargetId;
- }
- }
-
- if (!criteria.NextOnTrue.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnTrue;
- }
-
- if (!criteria.NextOnFalse.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnFalse;
- }
-
- if (!criteria.NextOnApprove.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnApprove;
- }
-
- if (!criteria.NextOnReject.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnReject;
- }
- }
-
- private static ListFormWorkflowCriteriaDto MapWorkflowCriteria(ListFormWorkflow criteria)
- {
- return new ListFormWorkflowCriteriaDto
- {
- Id = criteria.Id,
- ListFormCode = criteria.ListFormCode,
- Kind = criteria.Kind,
- Title = criteria.Title,
- CompareColumn = criteria.CompareColumn,
- CompareOperator = criteria.CompareOperator,
- CompareValue = criteria.CompareValue,
- Approver = criteria.Approver,
- NextOnStart = criteria.NextOnStart,
- NextOnTrue = criteria.NextOnTrue,
- NextOnFalse = criteria.NextOnFalse,
- NextOnApprove = criteria.NextOnApprove,
- NextOnReject = criteria.NextOnReject,
- PositionX = criteria.PositionX,
- PositionY = criteria.PositionY,
- CompareOutcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson)
- };
- }
-
- private static List DeserializeCompareOutcomes(string json)
- {
- if (json.IsNullOrWhiteSpace())
- {
- return [];
- }
-
- try
- {
- return JsonSerializer.Deserialize>(json) ?? [];
- }
- catch
- {
- return [];
- }
+ return ListFormWorkflowCriteriaMapper.MapOrdered(
+ await listFormWorkflowRepository.GetListAsync(x => x.ListFormCode == listFormCode));
}
}
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs
index 61bc145f..cc007594 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data.Seeds;
+using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select;
@@ -43,6 +44,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private readonly IListFormAuthorizationManager authManager;
private readonly IListFormSelectAppService listFormSelectAppService;
private readonly IQueryManager queryManager;
+ private readonly IDynamicDataManager dynamicDataManager;
private readonly IdentityUserManager identityUserManager;
private readonly ISozsoftEmailSender erpEmailSender;
private readonly ISettingProvider settingProvider;
@@ -55,6 +57,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
IListFormAuthorizationManager authManager,
IListFormSelectAppService listFormSelectAppService,
IQueryManager queryManager,
+ IDynamicDataManager dynamicDataManager,
IdentityUserManager identityUserManager,
ISozsoftEmailSender erpEmailSender,
ISettingProvider settingProvider,
@@ -66,6 +69,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
this.authManager = authManager;
this.listFormSelectAppService = listFormSelectAppService;
this.queryManager = queryManager;
+ this.dynamicDataManager = dynamicDataManager;
this.identityUserManager = identityUserManager;
this.erpEmailSender = erpEmailSender;
this.settingProvider = settingProvider;
@@ -75,118 +79,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
[HttpGet("criteria")]
public async Task GetCriteriaAsync(string? listFormCode = null)
{
- var code = listFormCode;
- var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
- .OrderBy(x => x.Id)
- .ToList();
-
return new ListFormWorkflowStateDto
{
- Criteria = OrderWorkflowCriteria(criteria.Select(MapCriteria).ToList())
+ Criteria = ListFormWorkflowCriteriaMapper.MapOrdered(
+ await criteriaRepository.GetListAsync(x => x.ListFormCode == listFormCode))
};
}
- private static List OrderWorkflowCriteria(List criteria)
- {
- if (criteria.Count <= 1)
- {
- return criteria;
- }
-
- var criteriaById = criteria.ToDictionary(x => x.Id);
- var ordered = new List();
- var visited = new HashSet();
- var visiting = new HashSet();
-
- var roots = criteria
- .Where(x => x.Kind == "Start")
- .OrderBy(x => x.Id)
- .ToList();
-
- if (roots.Count == 0)
- {
- var referencedIds = criteria
- .SelectMany(GetWorkflowTargetIds)
- .Where(criteriaById.ContainsKey)
- .ToHashSet();
-
- roots = criteria
- .Where(x => !referencedIds.Contains(x.Id))
- .OrderBy(x => x.Id)
- .ToList();
- }
-
- foreach (var root in roots)
- {
- VisitWorkflowCriteria(root);
- }
-
- foreach (var item in criteria.OrderBy(x => x.Id))
- {
- VisitWorkflowCriteria(item);
- }
-
- return ordered;
-
- void VisitWorkflowCriteria(ListFormWorkflowCriteriaDto item)
- {
- if (visited.Contains(item.Id) || visiting.Contains(item.Id))
- {
- return;
- }
-
- visiting.Add(item.Id);
- ordered.Add(item);
-
- foreach (var targetId in GetWorkflowTargetIds(item))
- {
- if (criteriaById.TryGetValue(targetId, out var target))
- {
- VisitWorkflowCriteria(target);
- }
- }
-
- visiting.Remove(item.Id);
- visited.Add(item.Id);
- }
- }
-
- private static IEnumerable GetWorkflowTargetIds(ListFormWorkflowCriteriaDto criteria)
- {
- if (!criteria.NextOnStart.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnStart;
- }
-
- foreach (var outcome in criteria.CompareOutcomes ?? [])
- {
- if (!outcome.TargetId.IsNullOrWhiteSpace())
- {
- yield return outcome.TargetId;
- }
- }
-
- if (!criteria.NextOnTrue.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnTrue;
- }
-
- if (!criteria.NextOnFalse.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnFalse;
- }
-
- if (!criteria.NextOnApprove.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnApprove;
- }
-
- if (!criteria.NextOnReject.IsNullOrWhiteSpace())
- {
- yield return criteria.NextOnReject;
- }
- }
-
[HttpPost("criteria")]
public async Task SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
{
@@ -216,7 +115,10 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
criteria.PositionX = input.PositionX <= 0 ? 32 : input.PositionX;
criteria.PositionY = input.PositionY <= 0 ? 150 : input.PositionY;
var outcomes = NormalizeCompareOutcomeTargets(code, input.CompareOutcomes);
- criteria.CompareOutcomesJson = SerializeCompareOutcomes(outcomes);
+ criteria.CompareOutcomesJson = ListFormWorkflowCriteriaMapper.SerializeCompareOutcomes(outcomes);
+ criteria.RejectReasonJson = criteria.Kind == "Approval"
+ ? WorkflowRejectReasonDto.Serialize(NormalizeRejectReason(input.RejectReason))
+ : null;
if (criteria.Kind == "Compare" && outcomes.Count > 0)
{
@@ -233,7 +135,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await criteriaRepository.UpdateAsync(criteria, autoSave: true);
}
- return MapCriteria(criteria);
+ return ListFormWorkflowCriteriaMapper.Map(criteria);
}
[HttpDelete("criteria/{id}")]
@@ -272,7 +174,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
start.NextOnStart = compare.Id;
compare.NextOnTrue = approval.Id;
compare.NextOnFalse = inform.Id;
- compare.CompareOutcomesJson = SerializeCompareOutcomes([
+ compare.CompareOutcomesJson = ListFormWorkflowCriteriaMapper.SerializeCompareOutcomes([
new CompareOutcomeDto
{
Label = "Onay gerekir",
@@ -365,6 +267,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
{
clear[context.Workflow.ApprovalDescriptionFieldName] = null;
}
+ foreach (var targetField in context.Criteria
+ .Select(x => WorkflowRejectReasonDto.Deserialize(x.RejectReasonJson)?.TargetField)
+ .Where(x => !x.IsNullOrWhiteSpace())
+ .Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ clear[targetField!] = null;
+ }
await UpdateRowAsync(context, clear);
MergeRowValues(context.Row, clear);
@@ -375,7 +284,11 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.ResetStep", "Reset Step"), LocalizeNodeTitle(previousStatus)));
}
- var result = await RunUntilWaitAsync(context, start);
+ // Manuel başlatılan akışta sıfırlama kaydı taslağa döndürür; akış yeniden yalnızca
+ // "İş Akışı Başlat" ile başlar (durum alanı boş kaldığı için düğme yeniden açılır).
+ var result = context.Workflow.ApprovalIsManualStart
+ ? ToRunResult(context, null, waitingApproval: false, completed: false)
+ : await RunUntilWaitAsync(context, start);
await InsertWorkflowNoteAsync(
context,
$"{Localize("App.WorkflowNote.SubjectReset", "Workflow Reset")}: {LocalizeNodeTitle(start.Title)}",
@@ -395,6 +308,19 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return await DecisionSingleAsync(input, input.Keys);
}
+ [HttpGet("reject-reasons")]
+ public async Task> GetRejectReasonsAsync(string listFormCode, string criteriaId)
+ {
+ await EnsureCanUpdateAsync(listFormCode);
+
+ var criteria = await criteriaRepository.FindAsync(criteriaId);
+ var reason = criteria?.ListFormCode == listFormCode
+ ? WorkflowRejectReasonDto.Deserialize(criteria.RejectReasonJson)
+ : null;
+
+ return reason == null ? [] : await GetRejectReasonItemsAsync(listFormCode, reason);
+ }
+
private async Task DecisionSingleAsync(DecisionWorkflowDto input, object[]? keys)
{
var context = await CreateRunContextAsync(input.ListFormCode, keys);
@@ -428,6 +354,10 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
throw new UserFriendlyException("Bu kayıt için onay yetkiniz yok.");
}
+ var rejectReasonConfig = input.Approved ? null : WorkflowRejectReasonDto.Deserialize(current.RejectReasonJson);
+ var rejectReason = await ResolveRejectReasonAsync(context.ListFormCode, rejectReasonConfig, input.RejectReasonId);
+ var note = input.Note?.Trim() ?? string.Empty;
+
var update = new Dictionary();
if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace())
{
@@ -439,9 +369,14 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
}
if (!context.Workflow.ApprovalDescriptionFieldName.IsNullOrWhiteSpace())
{
- update[context.Workflow.ApprovalDescriptionFieldName] = input.Note ?? string.Empty;
+ update[context.Workflow.ApprovalDescriptionFieldName] =
+ string.Join(" - ", new[] { rejectReason?.Text, note }.Where(x => !x.IsNullOrWhiteSpace()));
context.UserUpdatedFields.Add(context.Workflow.ApprovalDescriptionFieldName);
}
+ if (rejectReason != null && !rejectReasonConfig!.TargetField.IsNullOrWhiteSpace())
+ {
+ update[rejectReasonConfig.TargetField] = rejectReason.Id;
+ }
if (update.Count > 0)
{
@@ -451,7 +386,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.DecisionBy", "Decision By"), ResolveCurrentUserDisplayName()));
- AddWorkflowDecisionRows(context, current, input.Approved, input.Note ?? string.Empty);
+ AddWorkflowDecisionRows(context, current, input.Approved, rejectReason?.Text, note);
var result = await RunUntilWaitAsync(context, next);
var decisionSubject = input.Approved
? Localize("App.WorkflowNote.SubjectApproved", "Workflow Approved")
@@ -493,10 +428,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private async Task CreateRunContextAsync(string listFormCode, object[]? keys)
{
var code = listFormCode;
- if (!await authManager.CanAccess(code, AuthorizationTypeEnum.Update))
- {
- throw new UserFriendlyException("Workflow işlemi için güncelleme yetkiniz yok.");
- }
+ await EnsureCanUpdateAsync(code);
var listForm = await listFormManager.GetUserListForm(code);
var workflow = DeserializeWorkflow(listForm.WorkflowJson);
@@ -805,13 +737,15 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
WorkflowRunContext context,
ListFormWorkflow current,
bool approved,
+ string? rejectReason,
string description)
{
var action = approved
? Localize("App.WorkflowNote.Approved", "Approved")
: Localize("App.WorkflowNote.Rejected", "Rejected");
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.Decision", "Decision"), $"{action}: {FormatNode(current)}"));
- context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.Description", "Description"), description ?? string.Empty));
+ context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.RejectReason", "Reject Reason"), rejectReason ?? string.Empty));
+ context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.Description", "Description"), description));
}
private void AddWorkflowNodeRows(
@@ -1096,7 +1030,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
if (current.Kind == "Compare")
{
- var outcomes = DeserializeCompareOutcomes(current.CompareOutcomesJson);
+ var outcomes = ListFormWorkflowCriteriaMapper.DeserializeCompareOutcomes(current.CompareOutcomesJson);
var matched = outcomes.FirstOrDefault(outcome =>
outcome.Conditions == null ||
outcome.Conditions.Count == 0 ||
@@ -1294,7 +1228,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
NextOnReject = string.Empty,
PositionX = positionX,
PositionY = positionY,
- CompareOutcomesJson = SerializeCompareOutcomes([])
+ CompareOutcomesJson = ListFormWorkflowCriteriaMapper.SerializeCompareOutcomes([])
};
await criteriaRepository.InsertAsync(criteria, autoSave: true);
@@ -1421,7 +1355,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
changed = true;
}
- var outcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson);
+ var outcomes = ListFormWorkflowCriteriaMapper.DeserializeCompareOutcomes(criteria.CompareOutcomesJson);
foreach (var outcome in outcomes.Where(x => x.TargetId == deletedId))
{
outcome.TargetId = null;
@@ -1430,35 +1364,12 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
if (changed)
{
- criteria.CompareOutcomesJson = SerializeCompareOutcomes(outcomes);
+ criteria.CompareOutcomesJson = ListFormWorkflowCriteriaMapper.SerializeCompareOutcomes(outcomes);
}
return changed;
}
- private static ListFormWorkflowCriteriaDto MapCriteria(ListFormWorkflow criteria)
- {
- return new ListFormWorkflowCriteriaDto
- {
- Id = criteria.Id,
- ListFormCode = criteria.ListFormCode,
- Kind = criteria.Kind,
- Title = criteria.Title,
- CompareColumn = criteria.CompareColumn,
- CompareOperator = criteria.CompareOperator,
- CompareValue = criteria.CompareValue,
- Approver = criteria.Approver,
- NextOnStart = criteria.NextOnStart,
- NextOnTrue = criteria.NextOnTrue,
- NextOnFalse = criteria.NextOnFalse,
- NextOnApprove = criteria.NextOnApprove,
- NextOnReject = criteria.NextOnReject,
- PositionX = criteria.PositionX,
- PositionY = criteria.PositionY,
- CompareOutcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson)
- };
- }
-
private static string NormalizeRequired(string value, string fallback)
{
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
@@ -1494,26 +1405,128 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return candidate;
}
- private static string SerializeCompareOutcomes(List outcomes)
+ private async Task EnsureCanUpdateAsync(string listFormCode)
{
- return JsonSerializer.Serialize(outcomes ?? []);
+ if (!await authManager.CanAccess(listFormCode, AuthorizationTypeEnum.Update))
+ {
+ throw new UserFriendlyException("Workflow işlemi için güncelleme yetkiniz yok.");
+ }
}
- private static List DeserializeCompareOutcomes(string json)
+ private static WorkflowRejectReasonDto? NormalizeRejectReason(WorkflowRejectReasonDto? input)
{
- if (json.IsNullOrWhiteSpace())
+ if (input?.IsConfigured != true)
{
- return [];
+ return null;
}
- try
+ return new WorkflowRejectReasonDto
{
- return JsonSerializer.Deserialize>(json) ?? [];
- }
- catch
+ TableName = input.TableName.Trim(),
+ ValueField = input.ValueField.Trim(),
+ TextField = input.TextField.Trim(),
+ TargetField = input.TargetField?.Trim() ?? string.Empty,
+ Required = input.Required
+ };
+ }
+
+ ///
+ /// Red kararındaki neden. İstemcinin gönderdiği değer tablodan doğrulanır; metin istemciden
+ /// alınmaz, kayda ve nota tablodaki hâli yazılır.
+ ///
+ private async Task ResolveRejectReasonAsync(
+ string listFormCode,
+ WorkflowRejectReasonDto? reason,
+ string? rejectReasonId)
+ {
+ if (reason == null)
{
- return [];
+ return null;
}
+
+ if (rejectReasonId.IsNullOrWhiteSpace())
+ {
+ return reason.Required
+ ? throw new UserFriendlyException(L["App.ListFormWorkflow.RejectReasonRequired"])
+ : null;
+ }
+
+ var items = await GetRejectReasonItemsAsync(listFormCode, reason);
+ return items.FirstOrDefault(x => string.Equals(x.Id, rejectReasonId.Trim(), StringComparison.OrdinalIgnoreCase))
+ ?? throw new UserFriendlyException(L["App.ListFormWorkflow.RejectReasonNotFound"]);
+ }
+
+ ///
+ /// Nedenler ekranın veri kaynağındaki tablodan okunur. Tablo ve kolon adları katalogdan çözülür,
+ /// tanımdaki metin sorguya girmez; kiracı, silinmiş ve pasif satırlar kolon varsa süzülür.
+ ///
+ private async Task> GetRejectReasonItemsAsync(
+ string listFormCode,
+ WorkflowRejectReasonDto reason)
+ {
+ var listForm = await listFormManager.GetUserListForm(listFormCode);
+ var (repository, connectionString, _) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
+
+ var table = (await repository.QueryAsync(
+ "SELECT table_schema AS \"SchemaName\", table_name AS \"TableName\", column_name AS \"ColumnName\" " +
+ "FROM information_schema.columns WHERE table_name = @TableName",
+ connectionString,
+ new Dictionary { ["@TableName"] = reason.TableName }))
+ .Cast>()
+ .Select(row => (
+ Schema: row["SchemaName"]?.ToString() ?? string.Empty,
+ Table: row["TableName"]?.ToString() ?? string.Empty,
+ Column: row["ColumnName"]?.ToString() ?? string.Empty))
+ .Where(x => !x.Schema.Contains('"') && !x.Table.Contains('"') && !x.Column.Contains('"'))
+ .GroupBy(x => (x.Schema, x.Table))
+ .FirstOrDefault()
+ ?? throw new UserFriendlyException(L["App.ListFormWorkflow.RejectReasonTableNotFound", reason.TableName]);
+
+ var columns = table.Select(x => x.Column).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ string Column(string name) => columns.TryGetValue(name, out var actual)
+ ? $"\"{actual}\""
+ : throw new UserFriendlyException(L["App.ListFormWorkflow.RejectReasonColumnNotFound", name, reason.TableName]);
+
+ var filters = new List();
+ var parameters = new Dictionary();
+ if (columns.Contains("TenantId"))
+ {
+ if (CurrentTenant.Id.HasValue)
+ {
+ filters.Add($"{Column("TenantId")} = @TenantId");
+ parameters["@TenantId"] = CurrentTenant.Id.Value;
+ }
+ else
+ {
+ filters.Add($"{Column("TenantId")} IS NULL");
+ }
+ }
+ if (columns.Contains("IsDeleted"))
+ {
+ filters.Add($"{Column("IsDeleted")} = @IsDeleted");
+ parameters["@IsDeleted"] = false;
+ }
+ if (columns.Contains("IsActive"))
+ {
+ filters.Add($"{Column("IsActive")} = @IsActive");
+ parameters["@IsActive"] = true;
+ }
+
+ var textColumn = Column(reason.TextField);
+ var sql = $"SELECT {Column(reason.ValueField)} AS \"Id\", {textColumn} AS \"Text\" " +
+ $"FROM \"{table.Key.Schema}\".\"{table.Key.Table}\"" +
+ (filters.Count > 0 ? $" WHERE {string.Join(" AND ", filters)}" : string.Empty) +
+ $" ORDER BY {textColumn}";
+
+ return (await repository.QueryAsync(sql, connectionString, parameters))
+ .Cast>()
+ .Select(row => new WorkflowRejectReasonItemDto
+ {
+ Id = Convert.ToString(row["Id"], CultureInfo.InvariantCulture) ?? string.Empty,
+ Text = row["Text"]?.ToString() ?? string.Empty
+ })
+ .Where(x => !x.Id.IsNullOrWhiteSpace())
+ .ToList();
}
private static WorkflowDto DeserializeWorkflow(string json)
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowCriteriaMapper.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowCriteriaMapper.cs
new file mode 100644
index 00000000..a7ef7877
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowCriteriaMapper.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Sozsoft.Platform.Entities;
+
+namespace Sozsoft.Platform.ListForms.Workflow;
+
+///
+/// Workflow düğümünün entity → DTO dönüşümü ve ekranda gösterim sırası; tasarımcı
+/// () ile liste seçeneklerini dönen servis aynı sonucu üretir.
+///
+internal static class ListFormWorkflowCriteriaMapper
+{
+ public static List MapOrdered(IEnumerable criteria)
+ {
+ return Order(criteria.OrderBy(x => x.Id).Select(Map).ToList());
+ }
+
+ public static ListFormWorkflowCriteriaDto Map(ListFormWorkflow criteria)
+ {
+ return new ListFormWorkflowCriteriaDto
+ {
+ Id = criteria.Id,
+ ListFormCode = criteria.ListFormCode,
+ Kind = criteria.Kind,
+ Title = criteria.Title,
+ CompareColumn = criteria.CompareColumn,
+ CompareOperator = criteria.CompareOperator,
+ CompareValue = criteria.CompareValue,
+ Approver = criteria.Approver,
+ NextOnStart = criteria.NextOnStart,
+ NextOnTrue = criteria.NextOnTrue,
+ NextOnFalse = criteria.NextOnFalse,
+ NextOnApprove = criteria.NextOnApprove,
+ NextOnReject = criteria.NextOnReject,
+ PositionX = criteria.PositionX,
+ PositionY = criteria.PositionY,
+ CompareOutcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson),
+ RejectReason = WorkflowRejectReasonDto.Deserialize(criteria.RejectReasonJson)
+ };
+ }
+
+ public static string SerializeCompareOutcomes(List? outcomes)
+ {
+ return JsonSerializer.Serialize(outcomes ?? []);
+ }
+
+ public static List DeserializeCompareOutcomes(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return [];
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize>(json) ?? [];
+ }
+ catch (JsonException)
+ {
+ return [];
+ }
+ }
+
+ /// Düğümler başlangıçtan akış yönünde sıralanır; bağlantısız düğümler sona eklenir.
+ private static List Order(List criteria)
+ {
+ if (criteria.Count <= 1)
+ {
+ return criteria;
+ }
+
+ var criteriaById = criteria.ToDictionary(x => x.Id);
+ var ordered = new List();
+ var visited = new HashSet();
+ var visiting = new HashSet();
+
+ var roots = criteria.Where(x => x.Kind == "Start").ToList();
+ if (roots.Count == 0)
+ {
+ var referencedIds = criteria
+ .SelectMany(GetTargetIds)
+ .Where(criteriaById.ContainsKey)
+ .ToHashSet();
+
+ roots = criteria.Where(x => !referencedIds.Contains(x.Id)).ToList();
+ }
+
+ foreach (var item in roots.Concat(criteria))
+ {
+ Visit(item);
+ }
+
+ return ordered;
+
+ void Visit(ListFormWorkflowCriteriaDto item)
+ {
+ if (visited.Contains(item.Id) || !visiting.Add(item.Id))
+ {
+ return;
+ }
+
+ ordered.Add(item);
+
+ foreach (var targetId in GetTargetIds(item))
+ {
+ if (criteriaById.TryGetValue(targetId, out var target))
+ {
+ Visit(target);
+ }
+ }
+
+ visiting.Remove(item.Id);
+ visited.Add(item.Id);
+ }
+ }
+
+ private static IEnumerable GetTargetIds(ListFormWorkflowCriteriaDto criteria)
+ {
+ return new[] { criteria.NextOnStart }
+ .Concat((criteria.CompareOutcomes ?? []).Select(outcome => outcome.TargetId))
+ .Concat([criteria.NextOnTrue, criteria.NextOnFalse, criteria.NextOnApprove, criteria.NextOnReject])
+ .Where(id => !string.IsNullOrWhiteSpace(id));
+ }
+}
diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs
index 2eb7de2d..41f47f8f 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs
+++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs
@@ -226,6 +226,7 @@ public class WizardDataSeeder(
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
+ RejectReasonJson = WorkflowRejectReasonDto.Serialize(criteria.RejectReason),
});
}
diff --git a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs
index 52ebfe2f..9709704e 100644
--- a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs
+++ b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs
@@ -241,7 +241,8 @@ public static class WizardConsts
bool AllowUpdating,
bool ConfirmDelete,
bool SendOnlyChangedFormValuesUpdate,
- bool AllowDetail = false) => JsonSerializer.Serialize(new
+ bool AllowDetail = false,
+ bool OpenDetailAfterInsert = false) => JsonSerializer.Serialize(new
{
Popup = new { Title = Title, Width = Width, Height = Height },
AllowDeleting = AllowDeleting,
@@ -249,7 +250,8 @@ public static class WizardConsts
AllowUpdating = AllowUpdating,
ConfirmDelete = ConfirmDelete,
SendOnlyChangedFormValuesUpdate = SendOnlyChangedFormValuesUpdate,
- AllowDetail = AllowDetail
+ AllowDetail = AllowDetail,
+ OpenDetailAfterInsert = OpenDetailAfterInsert
});
}
diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs
index 5ba53a9b..2a17607b 100644
--- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs
+++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs
@@ -27,4 +27,10 @@ public class ListFormWorkflow : Entity
public int PositionX { get; set; }
public int PositionY { get; set; }
public string CompareOutcomesJson { get; set; }
+
+ ///
+ /// Onay düğümünde red nedeninin seçileceği tablo ve kolonları (WorkflowRejectReasonDto);
+ /// boşsa red yalnızca açıklama ister.
+ ///
+ public string RejectReasonJson { get; set; }
}
diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs
index 1eca7331..4a0f0d71 100644
--- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs
+++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs
@@ -571,6 +571,7 @@ public class PlatformDbContext :
b.Property(x => x.PositionX).IsRequired();
b.Property(x => x.PositionY).IsRequired();
b.Property(x => x.CompareOutcomesJson).HasColumnType("nvarchar(max)");
+ b.Property(x => x.RejectReasonJson).HasColumnType("nvarchar(max)");
});
builder.Entity(b =>
diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.Designer.cs
index cae5867b..7e19f510 100644
--- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.Designer.cs
+++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.Designer.cs
@@ -4077,6 +4077,9 @@ namespace Sozsoft.Platform.Migrations
b.Property("PositionY")
.HasColumnType("int");
+ b.Property("RejectReasonJson")
+ .HasColumnType("nvarchar(max)");
+
b.Property("Title")
.IsRequired()
.HasMaxLength(250)
diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.cs
index 11a300be..e9690dab 100644
--- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.cs
+++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20261009171249_Initial.cs
@@ -3429,7 +3429,8 @@ namespace Sozsoft.Platform.Migrations
NextOnReject = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
PositionX = table.Column(type: "int", nullable: false),
PositionY = table.Column(type: "int", nullable: false),
- CompareOutcomesJson = table.Column(type: "nvarchar(max)", nullable: true)
+ CompareOutcomesJson = table.Column(type: "nvarchar(max)", nullable: true),
+ RejectReasonJson = table.Column(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs
index 30328bd4..d673b17c 100644
--- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs
+++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs
@@ -4074,6 +4074,9 @@ namespace Sozsoft.Platform.Migrations
b.Property("PositionY")
.HasColumnType("int");
+ b.Property("RejectReasonJson")
+ .HasColumnType("nvarchar(max)");
+
b.Property("Title")
.IsRequired()
.HasMaxLength(250)
diff --git a/claude.md b/claude.md
index caa9e18a..58975517 100644
--- a/claude.md
+++ b/claude.md
@@ -254,6 +254,13 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
alanın varsayılanı `FormFieldsDefaultValueDto`'da `CustomValueType: 5`, `Value` = numaratörün `Name`'i.
`@AUTONUMBER`, `MAX + 1` sorgusu ya da SQL ile numara üretilmez (boşluksuz numara açıkça istenmedikçe).
Ayrıntı: `lowcode.instructions.md` §4.3.5.2.
+24. **İş akışlı liste** → tablo, tasarımcının Workflow şablonundaki dört kolonla kurulur ve `WorkflowDto`
+ yalnız bunlara bağlanır: `ApprovalUserName` nvarchar(256), `ApprovalStatus` nvarchar(50),
+ `ApprovalDate` datetime, `ApprovalDescription` nvarchar(200) (PG: varchar/timestamp). Ayrı durum
+ kolonu (`Status`), Guid onaycı kolonu (`ApproverId`) ve red nedeni kolonu açılmaz: durum akışın
+ düğüm başlığıdır, red nedeni onay düğümünün `RejectReason` listesinden seçilip açıklamaya yazılır.
+ Bu dört alan popup düzenleme formunda yer almaz (akış yazar), listede kolon olarak görünür.
+ Ayrıntı: `lowcode.instructions.md` §4.7.
### Cross-cutting: touching one thing means touching these too
@@ -580,7 +587,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
`Abp.Identity.Permission{Groups,s}.json`; platformun kendi menü/yetki kayıtları da diğer liste
verisiyle aynı mekanizmadan (`ListFormSeedDataApplier`) geçer, ayrı bir seeder yoktur. `Order`
alanı bağımlılık sırasını kurar: route → modül → menü, yetki grubu → yetki, ülke grubu → ülke →
- şehir → ilçe (`App.Definitions.{CountryGroup,Country,City,District}.json`), bildirim tipi → kuralı
+ şehir → ilçe (`App.Definitions.{CountryGroup,Country,City,District}.json`), departman → onaycı (`App.Hr.Department` → `App.Definitions.Approver`), bildirim tipi → kuralı
(`App.Notifications.Notification{Types,Rules}.json`). Para birimi, hitap, AI bot
(`App.Definitions.{Currency,ContactTitle,AiBot}.json`), ayar tanımları (`App.SettingDefinitions.json`)
ve zamanlanmış işler (`App.BackgroundWorkers.RecurringJobs.json`), dil → metin
diff --git a/configs/seeds/data/App.Definitions.Approver.json b/configs/seeds/data/App.Definitions.Approver.json
index 0e4afe85..a27b4e28 100644
--- a/configs/seeds/data/App.Definitions.Approver.json
+++ b/configs/seeds/data/App.Definitions.Approver.json
@@ -2,20 +2,13 @@
"ListFormCode": "App.Definitions.Approver",
"KeyFieldName": "Id",
"GeneratedAt": "2026-10-09T18:22:21.5830178Z",
- "Order": 0,
+ "Order": 11,
"Rows": [
- {
- "Id": "4f91635b-6fa1-4547-b687-2f0c0e188a67",
- "DepartmentId": "8f14e45f-ea2d-4d9f-9f1d-1a2b3c4d5e6f",
- "ApprovalArea": "Purchase",
- "ApproverUserId": "77dc76f8-c567-974c-4bc6-3a2433f9216e",
- "IsActive": true
- },
{
"Id": "db014b58-ec8b-4a65-b406-078f66323b7c",
"DepartmentId": "8f14e45f-ea2d-4d9f-9f1d-1a2b3c4d5e6f",
"ApprovalArea": "Purchase",
- "ApproverUserId": "e065080e-be54-c62c-a272-3a243467aa16",
+ "ApproverUserId": "0b221f19-f587-b053-d903-3a24394b182b",
"IsActive": true
}
]
diff --git a/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json b/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json
index cb0ea4fe..c1a2749a 100644
--- a/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json
+++ b/configs/seeds/data/App.DevRequests/53294e77-a4fb-45e4-a80e-acf07b5d75be.json
@@ -10,8 +10,8 @@
"UserName": "system@sozsoft.com",
"RequestDate": null,
"Status": "Testing",
- "Title": "Satınalma -> Satınalma Talepleri",
- "Description": "
Satınalma Talepleri : Satınalma yapılması için departmantların iç ve dış olmak üzere talep ettikleri ürünlerin kaydıdır. Talep eden, Talep Eden Departman, Mal Alımı olduğu zaman konulacak Depo ve Konum bilgisi ve Talep Tarihi bilgilerinden sonra Ürün ve talep edilen miktar bilgisi girilmeli. Ürün tanımında yer alan Birim bilgisi yine sistemle entegre şekilde gelmektedir. Termin Tarihi ürünün ne zaman ihtiyaç olduğu ve Talep Nedeni olarak ne amaçla istediği belirtilmelidir. Yine bu kaydın üst kısmında Taslak, Departman Onayı, Satınalma Onayı, Onaylandı, İşlemde, Tamamlandı gibi durumları da onay mekanizması Workflow gibi bir yöntemle yönetmeliyiz. Talepten sonra hemen talep edenin Departman Onaycısı onaylayacak bir liste üzerinden görmeli ve onaylamalı. Bu kişi Adm_T_Approver entity içerisindeki Onay Yeri : Satınalma ve Departman bazında gösterilecek) Sonrasında kayıt Satınalma departmanına düşmeli ondan sonra bu kayıt Satınalma Sorumlusu kabul etmeli ve Satınalma ile ilgili teklif toplama süreci başlamalı.
Red Nedenleri : Bazı Satınalma talepleri hem departman onaycısı hemde satınalma tarafından reddedilebilir. Reddetme sebepleri aşağıadki gibidir. Bu liste Satınalma -> Tanımlamalar menüsüne yerleştirilmelidir.
",
+ "Title": "Satınalma -> Malzeme Talepleri",
+ "Description": "
Malzeme Talepleri : Satınalma yapılması için departmantların iç ve dış olmak üzere talep ettikleri ürünlerin kaydıdır. Talep eden, Talep Eden Departman, Mal Alımı olduğu zaman konulacak Depo ve Konum bilgisi ve Talep Tarihi bilgilerinden sonra Ürün ve talep edilen miktar bilgisi girilmeli. Ürün tanımında yer alan Birim bilgisi yine sistemle entegre şekilde gelmektedir. Termin Tarihi ürünün ne zaman ihtiyaç olduğu ve Talep Nedeni olarak ne amaçla istediği belirtilmelidir. Yine bu kaydın üst kısmında Taslak, Departman Onayı, Satınalma Onayı, Onaylandı, İşlemde, Tamamlandı gibi durumları da onay mekanizması Workflow gibi bir yöntemle yönetmeliyiz. Talepten sonra hemen talep edenin Departman Onaycısı onaylayacak bir liste üzerinden görmeli ve onaylamalı. Bu kişi Adm_T_Approver entity içerisindeki Onay Yeri : Satınalma ve Departman bazında gösterilecek) Sonrasında kayıt Satınalma departmanına düşmeli ondan sonra bu kayıt Satınalma Sorumlusu kabul etmeli ve Satınalma ile ilgili teklif toplama süreci başlamalı.
Red Nedenleri : Bazı Satınalma talepleri hem departman onaycısı hemde satınalma tarafından reddedilebilir. Reddetme sebepleri aşağıadki gibidir. Bu liste Satınalma -> Tanımlamalar menüsüne yerleştirilmelidir.
Departmanların iç/dış ihtiyaç duydukları malzemeleri satınalmaya iletmesi, talebin önce departman onaycısı sonra satınalma tarafından onaylanması ve onaylı talebin satınalma sorumlusuna (teklif toplama/planlama) düşmesi. Kullanıcılar: talep eden personel, departman onaycıları (Adm_T_Approver › Onay Yeri = Satınalma), satınalma ekibi.
\n\n
2. Görsel analizi
\n
[image-1] Talep formu (başlık + satır sekmesi). Üstte durum çubuğu: Taslak › Departman Onayı › Satınalma Onayı › Onaylandı › İşlemde › Kısmi Tamamlandı › Tamamlandı. Başlık: belge no (SRO/00001 → bizde MT-YYYY-00001), öncelik yıldızları (0–3), Talep Eden (kullanıcı), Departman, Genel Açıklama, Depo, Konum, Şirket (kiracı — alan açılmaz), Talep Tarihi. Sekme \"Talep Satırları\": Ürün, Miktar, Birim, Karşılama Şekli, Termin Tarihi, Talep Nedeni, Kendi Depomda, Tüm Depolar, Teslim, Planlanacak Miktar, Satınalma Sorumlusu, satır durumu rozeti. Görünen satır demo veri adayı değildir (müşteri ortamına ait ürün kodu).
\n
[image-2] Onay / Red Bilgileri sekmesi. İki grup: Departman Onayı (Departman Onaycıları — etiket listesi, Onaylayan, Onay Tarihi, Onay Notu) ve Satınalma Onayı (Onaylayan, Onay Tarihi, Onay Notu). Tümü salt okunur, akış doldurur.
\n
[image-3] Red Nedenleri listesi. Basit tanım listesi: Red Nedeni, Aktif, sürükle-sırala tutamacı. Satırlar demo veri: İhtiyaç yok, Miktar uygun değil, Yanlış ürün seçildi, Bütçe uygun değil, Diğer (hepsi aktif).
\n\n
3. Çözüm yolu
\n
Wizard List (ComponentKind 0) + menüsüz alt form (satırlar) + Grid Workflow (§10.7 \"onaylı talep formu\" iskeleti). Aynı tablo üzerinde durum filtresiyle ayrılmış beş liste ekranı. Onaycı kullanıcı listeleri kayıt sonrası SQL prosedürüyle (save_sql_object) Adm_T_Approver'dan doldurulur ve akış düğümü @Kolon onaycısıyla okur (İzin Talepleri'ndeki @ManagerUserNames deseni). Serbest yerleşim gerekmediği için custom component yok.
ModuleId Prc, izin grubu App.Prc, route /admin/list/{ListFormCode}. Taleplerim talepte menü listesi olarak açıkça istendiği için persona kuralına rağmen menüde durur (girdi).
\n
Yetki: App.Prc › App.Prc.Definitions (Var) › App.Prc.RejectReason ; App.Prc › App.Prc.MaterialRequests (Yeni) › her liste ; alt form: App.Prc › App.Prc.MaterialRequests › {Liste} › App.Prc.MaterialRequestLine.
\n\n
5. Veri modeli
\n
Prc_T_RejectReason (yeni) — Tanım tablosu: Evet (talep \"Tanımlamalar menüsüne\" diyor, görselde Aktif kolonu). Kiracı + tam audit.
Onay / Red Bilgileri ayrı tablo değildir; ana formun ikinci düzenleme grubu (salt okunur alanlar) olarak çizilir. Alt form beş listenin hepsinde aynı ekrandır; ekleme/silme yalnız Status = Draft iken açıktır (alt formun Insert/Delete BeforeCommand'ında üst kayıt durumu kontrolü).
\n\n
7. Etkilenen kısımlar
\n
Yeni: 3 tablo, 1 prosedür (Prc_P_MaterialRequestResolveApprovers), 1 tanım ekranı, 1 alt form, 5 liste ekranı, ara menü App.Prc.MaterialRequests ve aynı kodlu ara yetki, her ekranın C/R/U/D/E/I/N yetkileri, dil anahtarları (alanlar, durum ve karşılama şekli statik seçenekleri), Adm_T_Sequence'e \"MaterialRequest\" satırı. Değişen: Onaycılar (App.Definitions.Approver) ApprovalArea seçeneği. İş akışı: iki onay düğümü + talep sahibine Inform (e-posta). Ürün, depo, konum, departman ekranlarına dokunulmaz.
\n\n
8. Görünümler ve davranış
\n
Liste filtreleri (aynı tablo, DefaultFilter): Taleplerim = RequesterUserId = @USERID (tüm durumlar; ekle açık); Departman Onayı Bekleyenler = Status = 'DeptApproval' + ApprovalIsFilterUserName; Satınalma Onayı Bekleyenler = Status = 'PurchaseApproval' + ApprovalIsFilterUserName; Planlama Bekleyenler = Status IN ('Approved','InProgress','PartiallyDone'); Tüm Talepler = filtresiz. Onay listelerinde ekleme kapalı.
\n
Görünümler: her listede Grid + Card + Pivot + Chart (açılış grid); Tüm Talepler'de ek olarak Todo (kanban) Status kolonuyla, durum sırası görseldeki çubukla aynı. Alt formda widget yok.
\n
Widget'lar: Tüm Talepler: Toplam · Departman Onayında · Satınalma Onayında · Onaylandı · Reddedildi. Taleplerim: Taleplerim · Onay Bekleyen · Onaylanan · Reddedilen. Red Nedenleri: Toplam · Aktif · Pasif.
\n
İş akışı (WorkflowDto, Taleplerim + iki onay listesi + Tüm Talepler'de aynı grafik): Start → Approval \"Departman Onayı\" (Approver = @DeptApproverUserNames) → onayda Approval \"Satınalma Onayı\" (Approver = @PurchaseApproverUserNames) → onayda Inform (talep sahibi) → End \"Onaylandı\"; her iki ret dalı → Inform → End \"Reddedildi\". ApprovalIsResetWorkflow = true (Taslak'a dönen kayıt düzenlenince akış baştan).
\n
Durum geçişleri: Kayıt Draft açılır; satır girildikten sonra toolbar butonu \"Onaya Gönder\" (CommandColumnDto, yalnız Draft ve en az bir satır varken) Status = DeptApproval yapar ve akışı başlatır. Prosedür (UpdateAfterCommand) akış düğümüne göre Status'u DeptApproval → PurchaseApproval → Approved / Rejected olarak senkronlar ve Dept*/Purchase* onay alanlarını ApproverId/ApprovalDate/ApprovalNote'tan kopyalar. Approved sonrası İşlemde / Kısmi Tamamlandı / Tamamlandı, Planlama Bekleyenler'deki satır butonlarıyla elle verilir; satırların tamamı LineStatus = Done olunca Status = Done, bir kısmı Done ise PartiallyDone (aynı prosedür).
Red: Ret notu zorunlu; RejectReasonId onay listelerinde onaycı tarafından düzenlenebilir alan, Rejected durumunda zorunlu (doğrulama prosedürde).
\n
EditorScript: RequestDate açılışta bugün; RequesterUserId açılışta @USERID ve DepartmentId kullanıcının departmanı; WarehouseId değişince LocationId temizlenir ve konum lookup'ı depoya süzülür (cascade); ProductId değişince UomId üründen dolar; Quantity değişince PlannedQuantity boşsa Quantity'ye eşitlenir; DueDate < RequestDate ise uyarı ve engel. Status ≠ Draft iken başlık alanları salt okunur.
\n\n
9. Kararlar
\n
\n
Belge no biçimi MT-YYYY-00001 (Odoo'daki SRO/ yerine platform numaratörü; İK ekranlarıyla tutarlı).
\n
\"Şirket\" alanı açılmaz; kiracı kapsamı karşılar.
\n
(onay) Satınalma onayçısı Adm_T_Approver'a eklenen yeni Onay Yeri PurchaseDesk (Satınalma Masası) satırlarından okunur; departmandan bağımsız, herhangi birinin onayı yeterli.
\n
(onay) Taslak aşaması için \"Onaya Gönder\" toolbar butonu; buton olmadan akış kayıtta hemen başlar ve Taslak durumu kullanılamaz.
\n
(onay) Görseldeki \"Kendi Depomda\" ve \"Tüm Depolar\" stok miktarı kolonları üretilmez: uygulamada stok miktarı/hareket tablosu yok (Inv_T_Product yalnız kart bilgisi taşır). Stok modülü gelince sorgu kolonu olarak eklenecek ayrı talep.
\n
(onay) Karşılama Şekli seçenekleri: Satınalma / Depodan Transfer.
\n
Satır durumu (rozet) LineStatus ile; başlık durumu satırlardan türetilir.
\n
Görseldeki \"Transferler\" akıllı butonu ve teklif toplama süreci bu talebin dışında (teklif/sipariş modülü yok); Planlama Bekleyenler listesi o sürecin giriş noktasıdır.
\n
Red Nedenleri demo verisi görseldeki 5 satır (SortOrder 1–5, aktif).
Şartlı — (onay) etiketli 4 karar: Satınalma onaycısı için PurchaseDesk onay yeri, \"Onaya Gönder\" butonuyla Taslak aşaması, stok miktarı kolonlarının (Kendi Depomda / Tüm Depolar) stok modülüne ertelenmesi, Karşılama Şekli seçenekleri.
MCP
\n
Üretilenler: tablolar Prc_T_RejectReason, Prc_T_MaterialRequest, Prc_T_MaterialRequestItem; prosedür Prc_P_MaterialRequestAfterSave (SQL Server + PostgreSQL); numaratör MaterialRequest (MT-{yyyy}-00001, yıllık); ekranlar App.Prc.RejectReason (5 demo satır), menüsüz alt form App.Prc.MaterialRequestLine, App.Prc.MyMaterialRequest, App.Prc.MaterialRequestDeptApproval, App.Prc.MaterialRequestPurchaseApproval, App.Prc.MaterialRequestPlanning, App.Prc.MaterialRequest; Onaycılar'a PurchaseDesk (Satınalma Masası) onay yeri; menü Satınalma › Satınalma Talepleri (yeni ara menü/yetki) ve Satınalma › Tanımlamalar › Red Nedenleri; modül dokümanı App.Prc › Satınalma Talepleri.
\n
Analizden sapmalar / varsayımlar:
\n
\n
Satır tablosu Prc_T_MaterialRequestItem adıyla açıldı: Adm_T_Uom.Id nvarchar olduğu için UomId nvarchar(64) olmalıydı; önce açılan Prc_T_MaterialRequestLine scripti (UomId uniqueidentifier) kullanılmıyor. Migrate'ten önce Wizard Manager / SQL Query Manager'dan bu scripti silmeniz önerilir (onayınız olmadan tablo düşürülmedi).
\n
Platform akışı onaylayanı kullanıcı adı (metin) olarak yazıyor; ApproverId uniqueidentifier olduğu için akışa bağlanmadı. Departman/Satınalma onaylayanı, tarihi ve notu prosedürde kaydı son güncelleyen kullanıcıdan ve akışın ApprovalDate/ApprovalNote alanlarından kopyalanır.
\n
Prosedür adı planda Prc_P_MaterialRequestResolveApprovers idi; onaycı çözümü + durum senkronu tek prosedürde: Prc_P_MaterialRequestAfterSave.
\n
\"Onaya Gönder\" ayrı buton olarak yazılmadı; akışın Start düğümü (platformun Onaya Gönder işlemi) kullanılır. İşlemde / Kısmi Tamamlandı / Tamamlandı satır durumlarından türetilir (ayrı satır butonu yok). Todo (kanban) görünümü eklenmedi.
\n
Onay listeleri ApprovalStatus (akış düğüm başlığı) ile süzülür; Status kayıt/satır kaydı sonrası prosedürle güncellenir. Akış onayı kayıt güncelleme komutunu tetiklemezse Status ve onaylayan bilgileri bir sonraki kayıtta güncellenir.
\n
Red nedeni zorunluluğu sunucuda zorlanmadı; alan onay listelerinde düzenlenebilir.
Departmanların iç/dış ihtiyaç duydukları malzemeleri satınalmaya iletmesi, talebin önce departman onaycısı sonra satınalma tarafından onaylanması ve onaylı talebin satınalma sorumlusuna (teklif toplama/planlama) düşmesi. Kullanıcılar: talep eden personel, departman onaycıları (Adm_T_Approver › Onay Yeri = Satınalma), satınalma ekibi.
\n\n
2. Görsel analizi
\n
[image-1] Talep formu (başlık + satır sekmesi). Üstte durum çubuğu: Taslak › Departman Onayı › Satınalma Onayı › Onaylandı › İşlemde › Kısmi Tamamlandı › Tamamlandı. Başlık: belge no (SRO/00001 → bizde MT-YYYY-00001), öncelik yıldızları (0–3), Talep Eden (kullanıcı), Departman, Genel Açıklama, Depo, Konum, Şirket (kiracı — alan açılmaz), Talep Tarihi. Sekme \"Talep Satırları\": Ürün, Miktar, Birim, Karşılama Şekli, Termin Tarihi, Talep Nedeni, Kendi Depomda, Tüm Depolar, Teslim, Planlanacak Miktar, Satınalma Sorumlusu, satır durumu rozeti. Görünen satır demo veri adayı değildir (müşteri ortamına ait ürün kodu).
\n
[image-2] Onay / Red Bilgileri sekmesi. İki grup: Departman Onayı (Departman Onaycıları — etiket listesi, Onaylayan, Onay Tarihi, Onay Notu) ve Satınalma Onayı (Onaylayan, Onay Tarihi, Onay Notu). Tümü salt okunur, akış doldurur.
\n
[image-3] Red Nedenleri listesi. Basit tanım listesi: Red Nedeni, Aktif, sürükle-sırala tutamacı. Satırlar demo veri: İhtiyaç yok, Miktar uygun değil, Yanlış ürün seçildi, Bütçe uygun değil, Diğer (hepsi aktif).
\n\n
3. Çözüm yolu
\n
Wizard List (ComponentKind 0) + menüsüz alt form (satırlar) + Grid Workflow (§10.7 \"onaylı talep formu\" iskeleti). Aynı tablo üzerinde durum filtresiyle ayrılmış beş liste ekranı. Onaycı kullanıcı listeleri kayıt sonrası SQL prosedürüyle (save_sql_object) Adm_T_Approver'dan doldurulur ve akış düğümü @Kolon onaycısıyla okur (İzin Talepleri'ndeki @ManagerUserNames deseni). Serbest yerleşim gerekmediği için custom component yok.
\n\n
4. Menü yeri
\n
Satınalma › Malzeme Talepleri › {5 liste} ; Satınalma › Tanımlamalar › Red Nedenleri
\n
Seviye
Kod
Ad (TR / EN)
İkon
Durum
\n
Kök
App.Prc
Satınalma / Purchasing
—
Var
\n
Ara
App.Prc.Definitions
Tanımlamalar / Definitions
—
Var
\n
Ekran
App.Prc.RejectReason
Red Nedenleri / Rejection Reasons
FcCancel
Yeni
\n
Ara
App.Prc.MaterialRequests
Malzeme Talepleri / Material Requests
FcList
Yeni
\n
Ekran
App.Prc.MyMaterialRequest
Taleplerim / My Requests
FcPortraitMode
Yeni
\n
Ekran
App.Prc.MaterialRequestDeptApproval
Departman Onayı Bekleyenler / Awaiting Department Approval
ModuleId Prc, izin grubu App.Prc, route /admin/list/{ListFormCode}. Taleplerim talepte menü listesi olarak açıkça istendiği için persona kuralına rağmen menüde durur (girdi).
\n
Yetki: App.Prc › App.Prc.Definitions (Var) › App.Prc.RejectReason ; App.Prc › App.Prc.MaterialRequests (Yeni) › her liste ; alt form: App.Prc › App.Prc.MaterialRequests › {Liste} › App.Prc.MaterialRequestLine.
\n\n
5. Veri modeli
\n
Prc_T_RejectReason (yeni) — Tanım tablosu: Evet (talep \"Tanımlamalar menüsüne\" diyor, görselde Aktif kolonu). Kiracı + tam audit.
Onay / Red Bilgileri ayrı tablo değildir; ana formun ikinci düzenleme grubu (salt okunur alanlar) olarak çizilir. Alt form beş listenin hepsinde aynı ekrandır; ekleme/silme yalnız Status = Draft iken açıktır (alt formun Insert/Delete BeforeCommand'ında üst kayıt durumu kontrolü).
\n\n
7. Etkilenen kısımlar
\n
Yeni: 3 tablo, 1 prosedür (Prc_P_MaterialRequestResolveApprovers), 1 tanım ekranı, 1 alt form, 5 liste ekranı, ara menü App.Prc.MaterialRequests ve aynı kodlu ara yetki, her ekranın C/R/U/D/E/I/N yetkileri, dil anahtarları (alanlar, durum ve karşılama şekli statik seçenekleri), Adm_T_Sequence'e \"MaterialRequest\" satırı. Değişen: Onaycılar (App.Definitions.Approver) ApprovalArea seçeneği. İş akışı: iki onay düğümü + talep sahibine Inform (e-posta). Ürün, depo, konum, departman ekranlarına dokunulmaz.
\n\n
8. Görünümler ve davranış
\n
Liste filtreleri (aynı tablo, DefaultFilter): Taleplerim = RequesterUserId = @USERID (tüm durumlar; ekle açık); Departman Onayı Bekleyenler = Status = 'DeptApproval' + ApprovalIsFilterUserName; Satınalma Onayı Bekleyenler = Status = 'PurchaseApproval' + ApprovalIsFilterUserName; Planlama Bekleyenler = Status IN ('Approved','InProgress','PartiallyDone'); Tüm Talepler = filtresiz. Onay listelerinde ekleme kapalı.
\n
Görünümler: her listede Grid + Card + Pivot + Chart (açılış grid); Tüm Talepler'de ek olarak Todo (kanban) Status kolonuyla, durum sırası görseldeki çubukla aynı. Alt formda widget yok.
\n
Widget'lar: Tüm Talepler: Toplam · Departman Onayında · Satınalma Onayında · Onaylandı · Reddedildi. Taleplerim: Taleplerim · Onay Bekleyen · Onaylanan · Reddedilen. Red Nedenleri: Toplam · Aktif · Pasif.
\n
İş akışı (WorkflowDto, Taleplerim + iki onay listesi + Tüm Talepler'de aynı grafik): Start → Approval \"Departman Onayı\" (Approver = @DeptApproverUserNames) → onayda Approval \"Satınalma Onayı\" (Approver = @PurchaseApproverUserNames) → onayda Inform (talep sahibi) → End \"Onaylandı\"; her iki ret dalı → Inform → End \"Reddedildi\". ApprovalIsResetWorkflow = true (Taslak'a dönen kayıt düzenlenince akış baştan).
\n
Durum geçişleri: Kayıt Draft açılır; satır girildikten sonra toolbar butonu \"Onaya Gönder\" (CommandColumnDto, yalnız Draft ve en az bir satır varken) Status = DeptApproval yapar ve akışı başlatır. Prosedür (UpdateAfterCommand) akış düğümüne göre Status'u DeptApproval → PurchaseApproval → Approved / Rejected olarak senkronlar ve Dept*/Purchase* onay alanlarını ApproverId/ApprovalDate/ApprovalNote'tan kopyalar. Approved sonrası İşlemde / Kısmi Tamamlandı / Tamamlandı, Planlama Bekleyenler'deki satır butonlarıyla elle verilir; satırların tamamı LineStatus = Done olunca Status = Done, bir kısmı Done ise PartiallyDone (aynı prosedür).
Red: Ret notu zorunlu; RejectReasonId onay listelerinde onaycı tarafından düzenlenebilir alan, Rejected durumunda zorunlu (doğrulama prosedürde).
\n
EditorScript: RequestDate açılışta bugün; RequesterUserId açılışta @USERID ve DepartmentId kullanıcının departmanı; WarehouseId değişince LocationId temizlenir ve konum lookup'ı depoya süzülür (cascade); ProductId değişince UomId üründen dolar; Quantity değişince PlannedQuantity boşsa Quantity'ye eşitlenir; DueDate < RequestDate ise uyarı ve engel. Status ≠ Draft iken başlık alanları salt okunur.
\n\n
9. Kararlar
\n
\n
Belge no biçimi MT-YYYY-00001 (Odoo'daki SRO/ yerine platform numaratörü; İK ekranlarıyla tutarlı).
\n
\"Şirket\" alanı açılmaz; kiracı kapsamı karşılar.
\n
(onay) Satınalma onayçısı Adm_T_Approver'a eklenen yeni Onay Yeri PurchaseDesk (Satınalma Masası) satırlarından okunur; departmandan bağımsız, herhangi birinin onayı yeterli.
\n
(onay) Taslak aşaması için \"Onaya Gönder\" toolbar butonu; buton olmadan akış kayıtta hemen başlar ve Taslak durumu kullanılamaz.
\n
(onay) Görseldeki \"Kendi Depomda\" ve \"Tüm Depolar\" stok miktarı kolonları üretilmez: uygulamada stok miktarı/hareket tablosu yok (Inv_T_Product yalnız kart bilgisi taşır). Stok modülü gelince sorgu kolonu olarak eklenecek ayrı talep.
\n
(onay) Karşılama Şekli seçenekleri: Satınalma / Depodan Transfer.
\n
Satır durumu (rozet) LineStatus ile; başlık durumu satırlardan türetilir.
\n
Görseldeki \"Transferler\" akıllı butonu ve teklif toplama süreci bu talebin dışında (teklif/sipariş modülü yok); Planlama Bekleyenler listesi o sürecin giriş noktasıdır.
\n
Red Nedenleri demo verisi görseldeki 5 satır (SortOrder 1–5, aktif).
Şartlı — (onay) etiketli 4 karar: Satınalma onaycısı için PurchaseDesk onay yeri, \"Onaya Gönder\" butonuyla Taslak aşaması, stok miktarı kolonlarının (Kendi Depomda / Tüm Depolar) stok modülüne ertelenmesi, Karşılama Şekli seçenekleri.
MCP
\n
Üretilenler: tablolar Prc_T_RejectReason, Prc_T_MaterialRequest, Prc_T_MaterialRequestItem; prosedür Prc_P_MaterialRequestAfterSave (SQL Server + PostgreSQL); numaratör MaterialRequest (MT-{yyyy}-00001, yıllık); ekranlar App.Prc.RejectReason (5 demo satır), menüsüz alt form App.Prc.MaterialRequestLine, App.Prc.MyMaterialRequest, App.Prc.MaterialRequestDeptApproval, App.Prc.MaterialRequestPurchaseApproval, App.Prc.MaterialRequestPlanning, App.Prc.MaterialRequest; Onaycılar'a PurchaseDesk (Satınalma Masası) onay yeri; menü Satınalma › Malzeme Talepleri (yeni ara menü/yetki) ve Satınalma › Tanımlamalar › Red Nedenleri; modül dokümanı App.Prc › Malzeme Talepleri.
\n
Analizden sapmalar / varsayımlar:
\n
\n
Satır tablosu Prc_T_MaterialRequestItem adıyla açıldı: Adm_T_Uom.Id nvarchar olduğu için UomId nvarchar(64) olmalıydı; önce açılan Prc_T_MaterialRequestLine scripti (UomId uniqueidentifier) kullanılmıyor. Migrate'ten önce Wizard Manager / SQL Query Manager'dan bu scripti silmeniz önerilir (onayınız olmadan tablo düşürülmedi).
\n
Platform akışı onaylayanı kullanıcı adı (metin) olarak yazıyor; ApproverId uniqueidentifier olduğu için akışa bağlanmadı. Departman/Satınalma onaylayanı, tarihi ve notu prosedürde kaydı son güncelleyen kullanıcıdan ve akışın ApprovalDate/ApprovalNote alanlarından kopyalanır.
\n
Prosedür adı planda Prc_P_MaterialRequestResolveApprovers idi; onaycı çözümü + durum senkronu tek prosedürde: Prc_P_MaterialRequestAfterSave.
\n
\"Onaya Gönder\" ayrı buton olarak yazılmadı; akışın Start düğümü (platformun Onaya Gönder işlemi) kullanılır. İşlemde / Kısmi Tamamlandı / Tamamlandı satır durumlarından türetilir (ayrı satır butonu yok). Todo (kanban) görünümü eklenmedi.
\n
Onay listeleri ApprovalStatus (akış düğüm başlığı) ile süzülür; Status kayıt/satır kaydı sonrası prosedürle güncellenir. Akış onayı kayıt güncelleme komutunu tetiklemezse Status ve onaylayan bilgileri bir sonraki kayıtta güncellenir.
\n
Red nedeni zorunluluğu sunucuda zorlanmadı; alan onay listelerinde düzenlenebilir.
Uygulamadaki tüm onay adımlarının onaycılarını tek listede toplamak: hangi departmanın hangi onay yerinde (Satınalma, Depo …) kimin onaylayacağı. İlk kullanıcısı Satınalma modülüdür (Satınalma Talepleri departman onayı); yöneticiler ve sistem sorumluları tanımlar.
\n\n
Çözüm yolu
\n
Tek Wizard List ekranı (tablo kaynaklı, popup düzenleme). Akışlar onaycıyı bu tablodan kayıt sonrası prosedürle okur ve @Kolon onaycısı olarak kullanır (§4.7); bu ekranın kendisinde iş akışı yoktur. Bu yapı, platform tablosu Hr_T_Department'a kolon eklemeyi gereksiz kılar.
→ Hr_T_Department; listede \"Şube / Departman\" adı
\n
ApprovalArea
nvarchar(32)
✓
Purchase / Warehouse / HumanResources / Finance (Satınalma / Depo / İnsan Kaynakları / Finans); DEFAULT 'Purchase'
\n
ApproverUserId
uniqueidentifier
✓
→ AbpUsers (aktif)
\n
IsActive
bit
✓
DEFAULT 1
\n
\n
Tekil kısıt: TenantId + DepartmentId + ApprovalArea + ApproverUserId. Aynı departman ve onay yerine birden çok onaycı girilebilir; akış bunları | ile birleştirip herhangi birinin onayını kabul eder.
\n\n
Etkilenen kısımlar
\n
\n
1 yeni tablo, 1 yeni ekran, Yönetim › Tanımlamalar menüsünde 1 satır; yetkiler C/R/U/D/E/I/N.
Var olan ekranlara dokunulmaz. \"Satınalma -> Satınalma Talepleri\" talebinin departman onaycısı bu tablodan (ApprovalArea = 'Purchase') okunacaktır; o talebin analizi bu ekrana göre güncellenmelidir (Hr_T_Department kolon ihtiyacı kalkar).
\n
\n\n
Görünümler ve davranış
\n
\n
Grid/Card/Pivot/Chart açık, varsayılan grid; liste Onay Yeri ve Departman'a göre gruplanabilir.
\n
Widget: Toplam / Aktif / Pasif.
\n
Departman lookup'ı aktif şubelerin departmanlarını \"Şube - Departman\" biçiminde gösterir; Onaylayacak Kişi aktif kullanıcıları ad soyad ile gösterir.
\n
Pasif satır akışlarda onaycı olarak kullanılmaz.
\n
\n\n
Kararlar
\n
\n
Ekran adı \"Onaycılar\", kod App.Definitions.Approver, tablo Adm_T_Approver — Yönetim tanımlarının kod/önek ailesi. Talep başlığı \"Satınalma Onaycıları\" olsa da açıklama tüm onayların burada toplanacağını söylüyor.
\n
Onay Yeri seçenekleri (onay): Satınalma, Depo, İnsan Kaynakları, Finans. Yeni yer eklemek seçenek listesine satır eklemektir.
\n
Şube kolonu tutulmaz; şube departmandan gelir.
\n
Demo veri yazılmaz (departman ve kullanıcı Id'leri kiracıya özgü).
\n
\n\n
Üretim planı
\n\n
create_table: Adm_T_Approver
\n
deploy_wizard: Approver.json
\n\n\n
Üretime hazır
\n
Evet
MCP
\n
\n
Tablo: Adm_T_Approver (DepartmentId FK → Hr_T_Department, ApprovalArea DEFAULT 'Purchase', ApproverUserId, IsActive DEFAULT 1); tekil TenantId+DepartmentId+ApprovalArea+ApproverUserId. Kiracı + tam audit.
Uygulamadaki tüm onay adımlarının onaycılarını tek listede toplamak: hangi departmanın hangi onay yerinde (Satınalma, Depo …) kimin onaylayacağı. İlk kullanıcısı Satınalma modülüdür (Malzeme Talepleri departman onayı); yöneticiler ve sistem sorumluları tanımlar.
\n\n
Çözüm yolu
\n
Tek Wizard List ekranı (tablo kaynaklı, popup düzenleme). Akışlar onaycıyı bu tablodan kayıt sonrası prosedürle okur ve @Kolon onaycısı olarak kullanır (§4.7); bu ekranın kendisinde iş akışı yoktur. Bu yapı, platform tablosu Hr_T_Department'a kolon eklemeyi gereksiz kılar.
→ Hr_T_Department; listede \"Şube / Departman\" adı
\n
ApprovalArea
nvarchar(32)
✓
Purchase / Warehouse / HumanResources / Finance (Satınalma / Depo / İnsan Kaynakları / Finans); DEFAULT 'Purchase'
\n
ApproverUserId
uniqueidentifier
✓
→ AbpUsers (aktif)
\n
IsActive
bit
✓
DEFAULT 1
\n
\n
Tekil kısıt: TenantId + DepartmentId + ApprovalArea + ApproverUserId. Aynı departman ve onay yerine birden çok onaycı girilebilir; akış bunları | ile birleştirip herhangi birinin onayını kabul eder.
\n\n
Etkilenen kısımlar
\n
\n
1 yeni tablo, 1 yeni ekran, Yönetim › Tanımlamalar menüsünde 1 satır; yetkiler C/R/U/D/E/I/N.
Var olan ekranlara dokunulmaz. \"Satınalma -> Malzeme Talepleri\" talebinin departman onaycısı bu tablodan (ApprovalArea = 'Purchase') okunacaktır; o talebin analizi bu ekrana göre güncellenmelidir (Hr_T_Department kolon ihtiyacı kalkar).
\n
\n\n
Görünümler ve davranış
\n
\n
Grid/Card/Pivot/Chart açık, varsayılan grid; liste Onay Yeri ve Departman'a göre gruplanabilir.
\n
Widget: Toplam / Aktif / Pasif.
\n
Departman lookup'ı aktif şubelerin departmanlarını \"Şube - Departman\" biçiminde gösterir; Onaylayacak Kişi aktif kullanıcıları ad soyad ile gösterir.
\n
Pasif satır akışlarda onaycı olarak kullanılmaz.
\n
\n\n
Kararlar
\n
\n
Ekran adı \"Onaycılar\", kod App.Definitions.Approver, tablo Adm_T_Approver — Yönetim tanımlarının kod/önek ailesi. Talep başlığı \"Satınalma Onaycıları\" olsa da açıklama tüm onayların burada toplanacağını söylüyor.
\n
Onay Yeri seçenekleri (onay): Satınalma, Depo, İnsan Kaynakları, Finans. Yeni yer eklemek seçenek listesine satır eklemektir.
\n
Şube kolonu tutulmaz; şube departmandan gelir.
\n
Demo veri yazılmaz (departman ve kullanıcı Id'leri kiracıya özgü).
\n
\n\n
Üretim planı
\n\n
create_table: Adm_T_Approver
\n
deploy_wizard: Approver.json
\n\n\n
Üretime hazır
\n
Evet
MCP
\n
\n
Tablo: Adm_T_Approver (DepartmentId FK → Hr_T_Department, ApprovalArea DEFAULT 'Purchase', ApproverUserId, IsActive DEFAULT 1); tekil TenantId+DepartmentId+ApprovalArea+ApproverUserId. Kiracı + tam audit.