Satınalma Talepleri düzenlemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-10-10 23:11:12 +03:00
parent 6f472b15f0
commit 2d48626219
68 changed files with 3476 additions and 5273 deletions

View file

@ -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` |

View file

@ -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

View file

@ -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)

View file

@ -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;
/// <summary>
/// Popup'tan eklenen kayıt kaydedilince listede kalmak yerine kaydın detay formuna (form sayfası) geçilir.
/// </summary>
public bool OpenDetailAfterInsert { get; set; } = false;
public bool UseIcons { get; set; } = false;
public bool ConfirmDelete { get; set; } = true;
/// <summary>Accepted Values: 'first' | 'last' | 'pageBottom' | 'pageTop' | 'viewportBottom' | 'viewportTop'

View file

@ -97,6 +97,8 @@ public class ListFormWizardDto
public bool AllowUpdating { get; set; }
public bool AllowDeleting { get; set; }
public bool AllowDetail { get; set; }
/// <summary>Popup'tan eklenen kayit kaydedilince detay formuna gecilir (<c>GridEditingDto.OpenDetailAfterInsert</c>).</summary>
public bool OpenDetailAfterInsert { get; set; }
public bool ConfirmDelete { get; set; }
/// <summary>
@ -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;

View file

@ -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),

View file

@ -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<CompareOutcomeDto> CompareOutcomes { get; set; } = [];
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public WorkflowRejectReasonDto RejectReason { get; set; }
}

View file

@ -7,5 +7,8 @@ public class DecisionWorkflowDto
public string CurrentNodeId { get; set; }
public bool Approved { get; set; }
public string Note { get; set; }
/// <summary>Reddin listeden seçilen nedeni; düğümün <see cref="WorkflowRejectReasonDto.ValueField"/> değeri.</summary>
public string RejectReasonId { get; set; }
}

View file

@ -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<WorkflowRunResultDto> StartAsync(StartWorkflowDto input);
Task<WorkflowRunResultDto> ResetAsync(StartWorkflowDto input);
Task<WorkflowRunResultDto> DecisionAsync(DecisionWorkflowDto input);
Task<List<WorkflowRejectReasonItemDto>> GetRejectReasonsAsync(string listFormCode, string criteriaId);
}

View file

@ -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<string>
public int PositionX { get; set; }
public int PositionY { get; set; }
public List<CompareOutcomeDto> CompareOutcomes { get; set; } = [];
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public WorkflowRejectReasonDto RejectReason { get; set; }
}

View file

@ -0,0 +1,51 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// 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 <see cref="ValueField"/>/<see cref="TextField"/>
/// kolonlarından seçenek listeler.
/// </summary>
public class WorkflowRejectReasonDto
{
public string TableName { get; set; }
public string ValueField { get; set; }
public string TextField { get; set; }
/// <summary>Seçilen nedenin değerinin yazılacağı kayıt kolonu; boşsa yalnızca not ve açıklamaya yazılır.</summary>
public string TargetField { get; set; }
public bool Required { get; set; } = true;
[JsonIgnore]
public bool IsConfigured =>
!string.IsNullOrWhiteSpace(TableName) &&
!string.IsNullOrWhiteSpace(ValueField) &&
!string.IsNullOrWhiteSpace(TextField);
/// <summary>Entity kolonu ile DTO arasındaki tek dönüşüm; yapılandırılmamış tanım <c>null</c> saklanır.</summary>
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<WorkflowRejectReasonDto>(json);
return value?.IsConfigured == true ? value : null;
}
catch (JsonException)
{
return null;
}
}
}

View file

@ -0,0 +1,7 @@
namespace Sozsoft.Platform.ListForms;
public class WorkflowRejectReasonItemDto
{
public string Id { get; set; }
public string Text { get; set; }
}

View file

@ -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<List<ListFormWorkflowCriteriaDto>> GetWorkflowCriteriaAsync(string listFormCode)
{
var criteria = await listFormWorkflowRepository.GetListAsync(x => x.ListFormCode == listFormCode);
return OrderWorkflowCriteria(criteria.Select(MapWorkflowCriteria).ToList());
}
private static List<ListFormWorkflowCriteriaDto> OrderWorkflowCriteria(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria.Count <= 1)
{
return criteria;
}
var criteriaById = criteria.ToDictionary(x => x.Id);
var ordered = new List<ListFormWorkflowCriteriaDto>();
var visited = new HashSet<string>();
var visiting = new HashSet<string>();
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<string> 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<CompareOutcomeDto> DeserializeCompareOutcomes(string json)
{
if (json.IsNullOrWhiteSpace())
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<CompareOutcomeDto>>(json) ?? [];
}
catch
{
return [];
}
return ListFormWorkflowCriteriaMapper.MapOrdered(
await listFormWorkflowRepository.GetListAsync(x => x.ListFormCode == listFormCode));
}
}

View file

@ -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<ListFormWorkflowStateDto> 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<ListFormWorkflowCriteriaDto> OrderWorkflowCriteria(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria.Count <= 1)
{
return criteria;
}
var criteriaById = criteria.ToDictionary(x => x.Id);
var ordered = new List<ListFormWorkflowCriteriaDto>();
var visited = new HashSet<string>();
var visiting = new HashSet<string>();
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<string> 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<ListFormWorkflowCriteriaDto> 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<List<WorkflowRejectReasonItemDto>> 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<WorkflowRunResultDto> 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<string, object?>();
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<WorkflowRunContext> 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<CompareOutcomeDto> 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<CompareOutcomeDto> 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<List<CompareOutcomeDto>>(json) ?? [];
TableName = input.TableName.Trim(),
ValueField = input.ValueField.Trim(),
TextField = input.TextField.Trim(),
TargetField = input.TargetField?.Trim() ?? string.Empty,
Required = input.Required
};
}
catch
/// <summary>
/// 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.
/// </summary>
private async Task<WorkflowRejectReasonItemDto?> ResolveRejectReasonAsync(
string listFormCode,
WorkflowRejectReasonDto? reason,
string? rejectReasonId)
{
return [];
if (reason == null)
{
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"]);
}
/// <summary>
/// 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.
/// </summary>
private async Task<List<WorkflowRejectReasonItemDto>> 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<string, object> { ["@TableName"] = reason.TableName }))
.Cast<IDictionary<string, object>>()
.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<string>();
var parameters = new Dictionary<string, object>();
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<IDictionary<string, object>>()
.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)

View file

@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Sozsoft.Platform.Entities;
namespace Sozsoft.Platform.ListForms.Workflow;
/// <summary>
/// Workflow düğümünün entity → DTO dönüşümü ve ekranda gösterim sırası; tasarımcı
/// (<see cref="ListFormWorkflowAppService"/>) ile liste seçeneklerini dönen servis aynı sonucu üretir.
/// </summary>
internal static class ListFormWorkflowCriteriaMapper
{
public static List<ListFormWorkflowCriteriaDto> MapOrdered(IEnumerable<ListFormWorkflow> 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<CompareOutcomeDto>? outcomes)
{
return JsonSerializer.Serialize(outcomes ?? []);
}
public static List<CompareOutcomeDto> DeserializeCompareOutcomes(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<CompareOutcomeDto>>(json) ?? [];
}
catch (JsonException)
{
return [];
}
}
/// <summary>Düğümler başlangıçtan akış yönünde sıralanır; bağlantısız düğümler sona eklenir.</summary>
private static List<ListFormWorkflowCriteriaDto> Order(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria.Count <= 1)
{
return criteria;
}
var criteriaById = criteria.ToDictionary(x => x.Id);
var ordered = new List<ListFormWorkflowCriteriaDto>();
var visited = new HashSet<string>();
var visiting = new HashSet<string>();
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<string> 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));
}
}

View file

@ -226,6 +226,7 @@ public class WizardDataSeeder(
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
RejectReasonJson = WorkflowRejectReasonDto.Serialize(criteria.RejectReason),
});
}

View file

@ -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
});
}

View file

@ -27,4 +27,10 @@ public class ListFormWorkflow : Entity<string>
public int PositionX { get; set; }
public int PositionY { get; set; }
public string CompareOutcomesJson { get; set; }
/// <summary>
/// Onay düğümünde red nedeninin seçileceği tablo ve kolonları (<c>WorkflowRejectReasonDto</c>);
/// boşsa red yalnızca açıklama ister.
/// </summary>
public string RejectReasonJson { get; set; }
}

View file

@ -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<Note>(b =>

View file

@ -4077,6 +4077,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("PositionY")
.HasColumnType("int");
b.Property<string>("RejectReasonJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(250)

View file

@ -3429,7 +3429,8 @@ namespace Sozsoft.Platform.Migrations
NextOnReject = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
PositionX = table.Column<int>(type: "int", nullable: false),
PositionY = table.Column<int>(type: "int", nullable: false),
CompareOutcomesJson = table.Column<string>(type: "nvarchar(max)", nullable: true)
CompareOutcomesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
RejectReasonJson = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{

View file

@ -4074,6 +4074,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("PositionY")
.HasColumnType("int");
b.Property<string>("RejectReasonJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(250)

View file

@ -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

View file

@ -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
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,12 @@
"Name": "Üretim",
"ParentId": "8f14e45f-ea2d-4d9f-9f1d-1a2b3c4d5e6f"
},
{
"Id": "2c1743a3-91b7-4b57-a6c8-7d8e9f0a112c",
"BranchId": "1c125bf3-3ffd-4445-a270-1bc43ac3f61c",
"Name": "Satınalma",
"ParentId": "8f14e45f-ea2d-4d9f-9f1d-1a2b3c4d5e6f"
},
{
"Id": "987bcab0-1b92-4a4e-8d7f-3c2b1a0f9e8d",
"BranchId": "1c125bf3-3ffd-4445-a270-1bc43ac3f61c",

View file

@ -13654,6 +13654,34 @@
"Key": "App.ListFormEdit.EditingAllowDuplicate",
"Value": "Çoğaltmaya İzin Ver"
},
{
"Id": "33bf5f3a-e6cb-5730-a4dd-0784e9849f4d",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.EditingGeneral",
"Value": "General"
},
{
"Id": "c8446544-7f1c-596d-84c9-8ca105a2990e",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.EditingGeneral",
"Value": "Genel"
},
{
"Id": "b20d74db-49bd-5ad1-a16d-c9eaae8d3bb0",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.EditingOpenDetailAfterInsert",
"Value": "Open Detail After Insert"
},
{
"Id": "24a24d50-ed42-53f0-8f06-19e8b8b455de",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.EditingOpenDetailAfterInsert",
"Value": "Kaydedince Detaya Git"
},
{
"Id": "c39c08f3-ced7-574c-9995-04e1c72ab1c3",
"CultureName": "en",
@ -92969,14 +92997,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Definitions.Approver.Desc",
"Value": "Approvers — the single list of who approves what for each department across the application.\nWhere: Administration › Definitions › Approvers.\nFields: department (shown as Branch - Department), approval area (Purchasing / Purchasing Desk / Warehouse / Human Resources / Finance), approver user, active.\nUsed by: approval workflows read the approvers of the record's department and area. Purchasing › Material Requests: the department approval uses area Purchasing for the requester's department; the purchasing approval uses area Purchasing Desk (any department). Several approvers may be defined for the same department and area; any one of them can approve. Passive rows are ignored by workflows."
"Value": "Approvers — the single list of who approves what for each department across the application.\nWhere: Administration › Definitions › Approvers.\nFields: department (shown as Branch - Department), approval area (Purchasing / Warehouse / Human Resources / Finance), approver user, active.\nUsed by: approval workflows read the approvers of the record's department and area. Purchasing › Material Requests: the department approval uses area Purchasing for the requester's department; the purchasing approval is not defined here: it goes to users with update permission on Awaiting Purchasing Approval (directly or through their role). Several approvers may be defined for the same department and area; any one of them can approve. Passive rows are ignored by workflows."
},
{
"Id": "3663c5aa-4036-5381-810f-8acee720724c",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Definitions.Approver.Desc",
"Value": "Onaycılar — uygulamadaki tüm onay adımlarında her departman için kimin onaylayacağını tutan tek liste.\nNerede: Yönetim › Tanımlamalar › Onaycılar.\nAlanlar: departman (Şube - Departman olarak gösterilir), onay yeri (Satınalma / Satınalma Masası / Depo / İnsan Kaynakları / Finans), onaylayacak kişi, aktif.\nKullanıldığı yer: onay akışları kaydın departmanı ve onay yerine göre onaycıları buradan okur. Satınalma › Satınalma Talepleri: departman onayı talep edenin departmanındaki Satınalma satırlarını, satınalma onayı ise Satınalma Masası satırlarını (departmandan bağımsız) kullanır. Aynı departman ve onay yerine birden çok onaycı girilebilir; herhangi birinin onayı yeterlidir. Pasif satırlar akışlarda kullanılmaz."
"Value": "Onaycılar — uygulamadaki tüm onay adımlarında her departman için kimin onaylayacağını tutan tek liste.\nNerede: Yönetim › Tanımlamalar › Onaycılar.\nAlanlar: departman (Şube - Departman olarak gösterilir), onay yeri (Satınalma / Depo / İnsan Kaynakları / Finans), onaylayacak kişi, aktif.\nKullanıldığı yer: onay akışları kaydın departmanı ve onay yerine göre onaycıları buradan okur. Satınalma › Malzeme Talepleri: departman onayı talep edenin departmanındaki Satınalma satırlarını, satınalma onayı burada tanımlanmaz: Satınalma Onayı Bekleyenler ekranında güncelleme yetkisi olan kullanıcılar (doğrudan ya da rolüyle) onaylar. Aynı departman ve onay yerine birden çok onaycı girilebilir; herhangi birinin onayı yeterlidir. Pasif satırlar akışlarda kullanılmaz."
},
{
"Id": "cd7ff644-eefa-516e-8c8c-8b4fa0ec76f8",
@ -93062,20 +93090,6 @@
"Key": "App.Listform.ListformField.ApproverUser",
"Value": "Onaylayacak Kişi"
},
{
"Id": "a0bd0fe5-757d-57a9-b689-e1b0e9af639e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.StaticLookup.PurchaseDesk",
"Value": "Purchasing Desk"
},
{
"Id": "ed5a8261-8b99-5809-b1fc-5012e9064ad4",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.StaticLookup.PurchaseDesk",
"Value": "Satınalma Masası"
},
{
"Id": "05f3d2d1-2824-5ec8-afa8-2631940be01b",
"CultureName": "en",
@ -93116,7 +93130,7 @@
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.RejectReason.Desc",
"Value": "Red Nedenleri — satınalma taleplerinin reddedilme sebepleri (ihtiyaç yok, miktar uygun değil, yanlış ürün, bütçe uygun değil, diğer).\nNerede: Satınalma › Tanımlamalar › Red Nedenleri.\nAlanlar: red nedeni, sıra (seçim listesindeki sırası), aktif.\nKullanıldığı yer: Satınalma › Satınalma Talepleri — departman onaycısı ya da satınalma talebi reddederken neden seçer. Yalnız aktif nedenler listelenir; kullanılmış neden silinmez, pasife alınır."
"Value": "Red Nedenleri — satınalma taleplerinin reddedilme sebepleri (ihtiyaç yok, miktar uygun değil, yanlış ürün, bütçe uygun değil, diğer).\nNerede: Satınalma › Tanımlamalar › Red Nedenleri.\nAlanlar: red nedeni, sıra (seçim listesindeki sırası), aktif.\nKullanıldığı yer: Satınalma › Malzeme Talepleri — departman onaycısı ya da satınalma talebi reddederken neden seçer. Yalnız aktif nedenler listelenir; kullanılmış neden silinmez, pasife alınır."
},
{
"Id": "31115331-7e79-54e9-ae55-a2e682571c43",
@ -93165,14 +93179,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestLine.Desc",
"Value": "Request Lines — the products requested in a material request.\nWhere: Purchasing › Material Requests › any list › Request Lines tab (no menu of its own).\nFields: product (active, purchasable products), quantity, unit (copied from the product's purchase unit, read-only), fulfilment (Purchasing / Warehouse Transfer), due date, request reason, delivered quantity (read-only), planned quantity (defaults to quantity), purchasing responsible, line status (Waiting / Planned / Done / Cancelled).\nRules: lines can be added or deleted only while the request has not been sent for approval. Changing line statuses recalculates the request status (In Progress / Partially Done / Done)."
"Value": "Request Lines — the products requested in a material request.\nWhere: Purchasing › Material Requests › My Requests, Awaiting Department Approval and Awaiting Purchasing Approval › Request Lines tab; All Requests shows the lines as columns (no menu of its own).\nFields: product (active, purchasable products), quantity, unit (copied from the product's purchase unit, read-only), fulfilment (Purchasing / Warehouse Transfer), due date, request reason, delivered quantity (read-only), planned quantity (defaults to quantity), purchasing responsible, line status (Waiting / Planned / Done / Cancelled).\nRules: lines can be added or deleted only while the request has not been sent for approval. Changing line statuses recalculates the request status (In Progress / Partially Done / Done)."
},
{
"Id": "cd985741-f49b-5a73-80a5-10f6ea403c1e",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestLine.Desc",
"Value": "Talep Satırları — malzeme talebinde istenen ürünler.\nNerede: Satınalma › Satınalma Talepleri › herhangi bir liste › Talep Satırları sekmesi (kendi menüsü yoktur).\nAlanlar: ürün (aktif ve satın alınabilir ürünler), miktar, birim (ürünün satınalma biriminden gelir, salt okunur), karşılama şekli (Satınalma / Depodan Transfer), termin tarihi, talep nedeni, teslim edilen miktar (salt okunur), planlanacak miktar (boşsa miktar), satınalma sorumlusu, satır durumu (Bekliyor / Planlandı / Tamamlandı / İptal).\nKurallar: satır yalnız talep onaya gönderilmeden eklenip silinebilir. Satır durumları değişince talebin durumu yeniden hesaplanır (İşlemde / Kısmi Tamamlandı / Tamamlandı)."
"Value": "Talep Satırları — malzeme talebinde istenen ürünler.\nNerede: Satınalma › Malzeme Talepleri › Taleplerim, Departman Onayı Bekleyenler ve Satınalma Onayı Bekleyenler › Talep Satırları sekmesi; Tüm Talepler satırları kolon olarak gösterir (kendi menüsü yoktur).\nAlanlar: ürün (aktif ve satın alınabilir ürünler), miktar, birim (ürünün satınalma biriminden gelir, salt okunur), karşılama şekli (Satınalma / Depodan Transfer), termin tarihi, talep nedeni, teslim edilen miktar (salt okunur), planlanacak miktar (boşsa miktar), satınalma sorumlusu, satır durumu (Bekliyor / Planlandı / Tamamlandı / İptal).\nKurallar: satır yalnız talep onaya gönderilmeden eklenip silinebilir. Satır durumları değişince talebin durumu yeniden hesaplanır (İşlemde / Kısmi Tamamlandı / Tamamlandı)."
},
{
"Id": "d6911bd6-3f4a-582d-8328-e01403808802",
@ -93347,14 +93361,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MyMaterialRequest.Desc",
"Value": "My Requests — the material requests you opened, in every status.\nWhere: Purchasing › Material Requests › My Requests.\nFields: request no (MT-YYYY-00001, automatic), priority, requester (you), department (your department by default), warehouse and location where the goods will be put, request date, status, general description; Request Lines tab: product, quantity, unit, fulfilment, due date, request reason.\nFlow: add lines, then send for approval — the department approver (Approvers › area Purchasing for your department) and then purchasing (area Purchasing Desk) approve or reject. Lines can only be added or removed before sending. Approval / rejection details are shown in the approval group.\nRelated: Awaiting Department Approval, Awaiting Purchasing Approval, Awaiting Planning, All Requests."
"Value": "My Requests — the material requests you opened, in every status.\nWhere: Purchasing › Material Requests › My Requests.\nFields: request no (MT-YYYY-00001, automatic), priority, requester (you), department (your department by default), warehouse and location where the goods will be put, request date, general description; Request Lines tab: product, quantity, unit, fulfilment, due date, request reason.\nFlow: add lines, then send for approval — the department approver (Approvers › area Purchasing for your department) and then purchasing (users with update permission on Awaiting Purchasing Approval (directly or through their role)) approve or reject. Lines can only be added or removed before sending. Approval status, approver, date and description (with the rejection reason) are filled by the workflow and shown in the list, not in the edit form.\nRelated: Awaiting Department Approval, Awaiting Purchasing Approval, All Requests."
},
{
"Id": "cdcb1b32-ecb7-5fd1-af5b-fdfd2614d779",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MyMaterialRequest.Desc",
"Value": "Taleplerim — açtığınız malzeme talepleri, tüm durumlarıyla.\nNerede: Satınalma › Satınalma Talepleri › Taleplerim.\nAlanlar: talep no (MT-YYYY-00001, otomatik), öncelik, talep eden (siz), departman (varsayılan olarak sizin departmanınız), malın konulacağı depo ve konum, talep tarihi, durum, genel açıklama; Talep Satırları sekmesi: ürün, miktar, birim, karşılama şekli, termin tarihi, talep nedeni.\nAkış: satırları girip onaya gönderin — önce departman onaycısı (Onaycılar › departmanınız için Satınalma onay yeri), ardından satınalma (Satınalma Masası onay yeri) onaylar ya da reddeder. Satırlar yalnız onaya göndermeden önce eklenip silinebilir. Onay / red bilgileri onay grubunda görünür.\nİlişkili: Departman Onayı Bekleyenler, Satınalma Onayı Bekleyenler, Planlama Bekleyenler, Tüm Talepler."
"Value": "Taleplerim — açtığınız malzeme talepleri, tüm durumlarıyla.\nNerede: Satınalma › Malzeme Talepleri › Taleplerim.\nAlanlar: talep no (MT-YYYY-00001, otomatik), öncelik, talep eden (siz), departman (varsayılan olarak sizin departmanınız), malın konulacağı depo ve konum, talep tarihi, genel açıklama; Talep Satırları sekmesi: ürün, miktar, birim, karşılama şekli, termin tarihi, talep nedeni.\nAkış: satırları girip onaya gönderin — önce departman onaycısı (Onaycılar › departmanınız için Satınalma onay yeri), ardından satınalma (Satınalma Onayı Bekleyenler ekranında güncelleme yetkisi olan kullanıcılar (doğrudan ya da rolüyle)) onaylar ya da reddeder. Satırlar yalnız onaya göndermeden önce eklenip silinebilir. Onay durumu, onaylayan, tarih ve açıklama (red nedeniyle birlikte) iş akışınca doldurulur; düzenleme formunda değil listede görünür.\nİlişkili: Departman Onayı Bekleyenler, Satınalma Onayı Bekleyenler, Tüm Talepler."
},
{
"Id": "ce3d734c-f508-5647-8f8d-2bf6b056c79a",
@ -93384,20 +93398,6 @@
"Key": "App.Prc.MaterialRequest.Group.Approval",
"Value": "Onay / Red Bilgileri"
},
{
"Id": "048c326c-7709-52a2-919f-928eb4f02fc2",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.Draft",
"Value": "Draft"
},
{
"Id": "e490d63f-4209-516f-9bf7-ded12e62c7e1",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.Draft",
"Value": "Taslak"
},
{
"Id": "3708a984-4c9b-536b-b4f8-3928bad0c787",
"CultureName": "en",
@ -93440,48 +93440,6 @@
"Key": "App.Prc.MaterialRequest.Status.Approved",
"Value": "Onaylandı"
},
{
"Id": "fc01609d-1b30-57e7-a8aa-a2ba025f8793",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.InProgress",
"Value": "In Progress"
},
{
"Id": "72fa09da-1c88-59b0-a11e-7d9ed3599ea4",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.InProgress",
"Value": "İşlemde"
},
{
"Id": "c1b67758-5e95-5dfd-81b7-4d601ee1a492",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.PartiallyDone",
"Value": "Partially Done"
},
{
"Id": "9dae2baf-ae05-5877-9194-e2ebacb20945",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.PartiallyDone",
"Value": "Kısmi Tamamlandı"
},
{
"Id": "0ae3f045-c580-5a1c-8ffb-2f34940230a0",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.Done",
"Value": "Done"
},
{
"Id": "ccda2c9c-6397-52dc-a159-5b3fd83f8820",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Status.Done",
"Value": "Tamamlandı"
},
{
"Id": "b29fee10-a751-5dec-9eb5-8ac8837d5ef8",
"CultureName": "en",
@ -93564,7 +93522,7 @@
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequests",
"Value": "Satınalma Talepleri"
"Value": "Malzeme Talepleri"
},
{
"Id": "cf76b42b-82ed-5838-9fb2-30b0c8caf3e5",
@ -93594,20 +93552,6 @@
"Key": "App.Listform.ListformField.LocationId",
"Value": "Konum"
},
{
"Id": "caada0e9-b75d-52f2-b07e-c25384a2532e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.RejectReasonId",
"Value": "Rejection Reason"
},
{
"Id": "787d4dd5-867b-52a0-8689-f55801750fe0",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.RejectReasonId",
"Value": "Red Nedeni"
},
{
"Id": "354564b7-be8f-5e32-9add-9fbda8f912f2",
"CultureName": "en",
@ -93636,34 +93580,6 @@
"Key": "App.Listform.ListformField.PurchaseApproverUserNames",
"Value": "Satınalma Onaycıları"
},
{
"Id": "6f297c45-bc1e-5235-a511-f96c07ef970c",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ApproverId",
"Value": "Approver Id"
},
{
"Id": "5d442093-8ce2-5d44-9ea1-4306060a8fd4",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ApproverId",
"Value": "Onaylayan Kimlik"
},
{
"Id": "8647efbf-f775-5e53-a00e-f1ce508c609f",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ApprovalNote",
"Value": "Approval Note"
},
{
"Id": "9d232928-5938-5b1d-b3c3-d752e1cd2077",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ApprovalNote",
"Value": "Onay Notu"
},
{
"Id": "3cb0bb36-408f-50e8-b260-850cbdd25e7a",
"CultureName": "en",
@ -93678,90 +93594,6 @@
"Key": "App.Listform.ListformField.DeptApproverUserNames",
"Value": "Departman Onaycıları"
},
{
"Id": "8e05f767-5e90-5aed-8e15-f544883da7b4",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovedBy",
"Value": "Purchasing Approved By"
},
{
"Id": "5294b27e-2e61-5c0c-8b69-406425b5f592",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovedBy",
"Value": "Satınalma Onaylayan"
},
{
"Id": "7697f43d-e5da-5666-b919-ae0eb6b7a6a0",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovedBy",
"Value": "Department Approved By"
},
{
"Id": "f56eb927-0d12-5192-bf36-edb5847aae45",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovedBy",
"Value": "Departman Onaylayan"
},
{
"Id": "b71494eb-3a96-5f3d-bf22-87421cad6f5c",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovalDate",
"Value": "Purchasing Approval Date"
},
{
"Id": "d4e99bb0-b797-5dd1-a4f8-18163b533b5b",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovalDate",
"Value": "Satınalma Onay Tarihi"
},
{
"Id": "423c12ac-9935-54f0-8973-9e508cc0961a",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovalDate",
"Value": "Department Approval Date"
},
{
"Id": "c86753a5-1b33-5b79-8fe4-55a8dfd87e88",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovalDate",
"Value": "Departman Onay Tarihi"
},
{
"Id": "46d7172f-46ca-5b5c-af1b-01b30e4e78e9",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovalNote",
"Value": "Purchasing Approval Note"
},
{
"Id": "1615218a-57a7-5398-b0d3-1f9eef0039db",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.PurchaseApprovalNote",
"Value": "Satınalma Onay Notu"
},
{
"Id": "08ed225a-1c13-5c79-970e-7ec77930277a",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovalNote",
"Value": "Department Approval Note"
},
{
"Id": "238cae24-91e7-5c8e-82a7-ba6807076cdd",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.DeptApprovalNote",
"Value": "Departman Onay Notu"
},
{
"Id": "6992315a-5810-5cf8-9061-6c9f8b61d750",
"CultureName": "en",
@ -93795,14 +93627,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestDeptApproval.Desc",
"Value": "Awaiting Department Approval — material requests waiting for the department approver's decision.\nWhere: Purchasing › Material Requests › Awaiting Department Approval.\nWho: approvers defined in Administration › Definitions › Approvers with area Purchasing for the requester's department; each approver sees only the requests assigned to them.\nWhat to do: open the request, review the Request Lines tab, then Approve (the request moves to purchasing approval) or Reject with a note and a rejection reason (Purchasing › Definitions › Rejection Reasons). Lines cannot be added or removed here.\nRelated: My Requests, Awaiting Purchasing Approval, All Requests."
"Value": "Awaiting Department Approval — material requests waiting for the department approver's decision.\nWhere: Purchasing › Material Requests › Awaiting Department Approval.\nWho: approvers defined in Administration › Definitions › Approvers with area Purchasing for the requester's department; each approver sees only the requests assigned to them.\nWhat to do: open the request, review the Request Lines tab, then Approve (the request moves to purchasing approval) or Reject by choosing a rejection reason (Purchasing › Definitions › Rejection Reasons) and an optional note; the reason and note are written to the approval description. Lines cannot be added or removed here.\nRelated: My Requests, Awaiting Purchasing Approval, All Requests."
},
{
"Id": "3467add9-46c6-59ea-b3ea-19d8524bb6bb",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestDeptApproval.Desc",
"Value": "Departman Onayı Bekleyenler — departman onaycısının kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Satınalma Talepleri › Departman Onayı Bekleyenler.\nKim: Yönetim › Tanımlamalar › Onaycılar'da talep edenin departmanı için Satınalma onay yerinde tanımlı onaycılar; her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi açıp Talep Satırları sekmesini inceleyin, ardından Onayla (talep satınalma onayına geçer) ya da not ve red nedeniyle (Satınalma › Tanımlamalar › Red Nedenleri) Reddet. Burada satır eklenip silinemez.\nİlişkili: Taleplerim, Satınalma Onayı Bekleyenler, Tüm Talepler."
"Value": "Departman Onayı Bekleyenler — departman onaycısının kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Malzeme Talepleri › Departman Onayı Bekleyenler.\nKim: Yönetim › Tanımlamalar › Onaycılar'da talep edenin departmanı için Satınalma onay yerinde tanımlı onaycılar; her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi açıp Talep Satırları sekmesini inceleyin, ardından Onayla (talep satınalma onayına geçer) ya da red nedeni (Satınalma › Tanımlamalar › Red Nedenleri) seçip isteğe bağlı notla Reddet; neden ve not onay açıklamasına yazılır. Burada satır eklenip silinemez.\nİlişkili: Taleplerim, Satınalma Onayı Bekleyenler, Tüm Talepler."
},
{
"Id": "1c9a636f-fdfa-50bf-b3a4-874deed5dad1",
@ -93837,14 +93669,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestPurchaseApproval.Desc",
"Value": "Awaiting Purchasing Approval — material requests approved by the department and waiting for purchasing.\nWhere: Purchasing › Material Requests › Awaiting Purchasing Approval.\nWho: approvers defined in Administration › Definitions › Approvers with area Purchasing Desk (any department); each approver sees only the requests assigned to them.\nWhat to do: review the request and its lines, then Approve (the request becomes Approved and appears in Awaiting Planning, the requester is notified) or Reject with a note and a rejection reason.\nRelated: Awaiting Department Approval, Awaiting Planning, All Requests."
"Value": "Awaiting Purchasing Approval — material requests approved by the department and waiting for purchasing.\nWhere: Purchasing › Material Requests › Awaiting Purchasing Approval.\nWho: users with update permission on Awaiting Purchasing Approval (directly or through their role); the approver list of each request is refreshed when the request is saved, and each approver sees only the requests assigned to them.\nWhat to do: review the request and its lines, then Approve (the request becomes Approved, the requester is notified) or Reject by choosing a rejection reason and an optional note; both are written to the approval description.\nRelated: Awaiting Department Approval, All Requests."
},
{
"Id": "bbcdb2c8-96fc-5ae3-b948-1d68b8a334c5",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequestPurchaseApproval.Desc",
"Value": "Satınalma Onayı Bekleyenler — departmanca onaylanmış ve satınalmanın kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Satınalma Talepleri › Satınalma Onayı Bekleyenler.\nKim: Yönetim › Tanımlamalar › Onaycılar'da Satınalma Masası onay yerinde tanımlı onaycılar (departmandan bağımsız); her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi ve satırlarını inceleyip Onayla (talep Onaylandı olur, Planlama Bekleyenler'e düşer, talep edene bilgi gider) ya da not ve red nedeniyle Reddet.\nİlişkili: Departman Onayı Bekleyenler, Planlama Bekleyenler, Tüm Talepler."
"Value": "Satınalma Onayı Bekleyenler — departmanca onaylanmış ve satınalmanın kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Malzeme Talepleri › Satınalma Onayı Bekleyenler.\nKim: Satınalma Onayı Bekleyenler ekranında güncelleme yetkisi olan kullanıcılar (doğrudan ya da rolüyle); talebin onaycı listesi talep kaydedilince yenilenir, her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi ve satırlarını inceleyip Onayla (talep Onaylandı olur, talep edene bilgi gider) ya da red nedeni seçip isteğe bağlı notla Reddet; ikisi de onay açıklamasına yazılır.\nİlişkili: Departman Onayı Bekleyenler, Tüm Talepler."
},
{
"Id": "04e8c193-a841-520d-b338-0d4a73ca066b",
@ -93879,14 +93711,14 @@
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Desc",
"Value": "All Requests — every material request of the company in every status; used by purchasing management.\nWhere: Purchasing › Material Requests › All Requests.\nFields: request no (MT-YYYY-00001, automatic), priority, requester, department, warehouse and location, request date, status (Draft › Department Approval › Purchasing Approval › Approved › In Progress › Partially Done › Done, or Rejected), approval status, rejection reason, general description; approval group: department approvers, department / purchasing approver, date and note; Request Lines tab.\nFlow: Grid Workflow — department approver (Approvers › area Purchasing of the request's department) → purchasing (area Purchasing Desk) → Approved / Rejected; the requester is notified. Status after approval follows the line statuses.\nRelated: My Requests, Awaiting Department Approval, Awaiting Purchasing Approval, Awaiting Planning, Purchasing › Definitions › Rejection Reasons."
"Value": "All Requests — every material request of the company in every status, listed per request line; list of purchasing management.\nWhere: Purchasing › Material Requests › All Requests.\nFields: each row is one request line — request header (request no, priority, requester, department, warehouse and location, request date, general description, approval status / approver / date / description) next to the line (product, quantity, unit, fulfillment method, due date, reason, delivered quantity, purchasing responsible, line status). A request without lines is shown as a single row with empty line columns.\nFlow: requests are created in My Requests and approved in Awaiting Department Approval › Awaiting Purchasing Approval; this list does not add or delete; header fields and the approval status written by that workflow are read-only, editing a row only sets the line's purchasing responsible and line status. Cards count requests, not lines.\nRelated: My Requests, Awaiting Department Approval, Awaiting Purchasing Approval, Purchasing › Definitions › Rejection Reasons."
},
{
"Id": "b9ef44e8-a325-544f-b552-bda2fb6ca9c1",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Prc.MaterialRequest.Desc",
"Value": "Tüm Talepler — kurumun bütün malzeme talepleri, tüm durumlarıyla; satınalma yönetiminin listesi.\nNerede: Satınalma › Satınalma Talepleri › Tüm Talepler.\nAlanlar: talep no (MT-YYYY-00001, otomatik), öncelik, talep eden, departman, depo ve konum, talep tarihi, durum (Taslak › Departman Onayı › Satınalma Onayı › Onaylandı › İşlemde › Kısmi Tamamlandı › Tamamlandı ya da Reddedildi), onay durumu, red nedeni, genel açıklama; onay grubu: departman onaycıları, departman / satınalma onaylayanı, tarihi ve notu; Talep Satırları sekmesi.\nAkış: Grid Workflow — departman onaycısı (Onaycılar › talebin departmanı için Satınalma onay yeri) → satınalma (Satınalma Masası onay yeri) → Onaylandı / Reddedildi; talep edene bilgi gider. Onay sonrası durum satır durumlarını izler.\nİlişkili: Taleplerim, Departman Onayı Bekleyenler, Satınalma Onayı Bekleyenler, Planlama Bekleyenler, Satınalma › Tanımlamalar › Red Nedenleri."
"Value": "Tüm Talepler — kurumun bütün malzeme talepleri, tüm durumlarıyla, kalem kalem; satınalma yönetiminin listesi.\nNerede: Satınalma › Malzeme Talepleri › Tüm Talepler.\nAlanlar: her satır bir talep kalemidir — talep başlığı (talep no, öncelik, talep eden, departman, depo ve konum, talep tarihi, genel açıklama, onay durumu / onaylayan / tarih / açıklama) ile kalemin bilgileri (ürün, miktar, birim, karşılama yöntemi, termin, talep nedeni, teslim edilen miktar, satınalma sorumlusu, satır durumu) yan yanadır. Kalemi olmayan talep, kalem kolonları boş tek satır olarak görünür.\nAkış: talep Taleplerim ekranında açılır, Departman Onayı Bekleyenler › Satınalma Onayı Bekleyenler ekranlarında onaylanır; bu liste ekleme ve silme yapmaz; başlık alanları ve o iş akışının yazdığı onay durumu salt okunurdur, satırı düzenlemek yalnız kalemin satınalma sorumlusunu ve satır durumunu değiştirir. Kartlar kalemi değil talebi sayar.\nİlişkili: Taleplerim, Departman Onayı Bekleyenler, Satınalma Onayı Bekleyenler, Satınalma › Tanımlamalar › Red Nedenleri."
},
{
"Id": "3d2aa673-6148-5445-9fb8-c260913a910f",
@ -93929,6 +93761,202 @@
"ResourceName": "Platform",
"Key": "App.Tenant.Name",
"Value": "Kiracı Adı"
},
{
"Id": "4b3114be-222a-5abf-ae77-d2ad2a321e7a",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.DesignTable",
"Value": "Design Table"
},
{
"Id": "95839291-2e69-5129-9796-904fd18dbcee",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormEdit.DesignTable",
"Value": "Tablo Tasarımı"
},
{
"Id": "973de671-71f2-547d-97e7-fe6277cf08ed",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.WorkflowNote.RejectReason",
"Value": "Reject Reason"
},
{
"Id": "89ce6320-14b2-54ce-b2fc-9ec20fa9361f",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.WorkflowNote.RejectReason",
"Value": "Red Nedeni"
},
{
"Id": "20cc436a-9277-55bf-8e07-2a79179af9a4",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonRequired",
"Value": "A reject reason must be selected."
},
{
"Id": "5b8cab4e-6616-5681-bd2d-68e42568cdd7",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonRequired",
"Value": "Red nedeni seçilmelidir."
},
{
"Id": "1c5d7dde-28cc-5b2f-8f3c-b6a31a981174",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonNotFound",
"Value": "The selected reject reason was not found."
},
{
"Id": "c709ef5d-57bb-518c-b67d-6182113131c1",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonNotFound",
"Value": "Seçilen red nedeni bulunamadı."
},
{
"Id": "523e57cb-24fe-5087-8682-6adf60ee54e5",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTableNotFound",
"Value": "Reject reason table was not found: {0}"
},
{
"Id": "1f7a2c1b-20c1-5717-8c5b-32cba96482b3",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTableNotFound",
"Value": "Red nedeni tablosu bulunamadı: {0}"
},
{
"Id": "8b9dcc7a-3d32-5cec-a94b-beb86f4c6900",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonColumnNotFound",
"Value": "Column '{0}' was not found in reject reason table '{1}'."
},
{
"Id": "3f9232e6-4791-5489-a24b-422fdaa574c5",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonColumnNotFound",
"Value": "'{1}' red nedeni tablosunda '{0}' kolonu yok."
},
{
"Id": "3bfaf9e8-3913-590d-8636-5cc69c10990c",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonSection",
"Value": "Reject Reason List"
},
{
"Id": "3be0012c-ebc7-562c-8585-1fc376aef1c0",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonSection",
"Value": "Red Nedeni Listesi"
},
{
"Id": "27ff219a-0dfe-50b9-98da-ee9564329905",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonHint",
"Value": "Leave the table empty to ask only for a description on reject."
},
{
"Id": "f6ddb820-f67d-5073-8a59-feaa977be779",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonHint",
"Value": "Tablo boş bırakılırsa red sırasında yalnızca açıklama sorulur."
},
{
"Id": "966f9fd2-8a64-547b-8f2b-7c39508b1837",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTable",
"Value": "Table"
},
{
"Id": "6da2c479-85f2-5885-beb0-08fdce43abc6",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTable",
"Value": "Tablo"
},
{
"Id": "13863cf5-8d59-57cc-a4ae-e3100fc0616d",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonValueField",
"Value": "Id Column"
},
{
"Id": "bbfba405-1d51-551e-943f-9bcb67af3e0d",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonValueField",
"Value": "Id Kolonu"
},
{
"Id": "4da6dc9f-68a7-5f5a-8153-b3b340372684",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTextField",
"Value": "Text Column"
},
{
"Id": "c439c7f8-3cae-54ea-87b7-a6ce2da43e0e",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTextField",
"Value": "Metin Kolonu"
},
{
"Id": "a10b8a0d-6a04-5043-893b-da40f739f306",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTargetField",
"Value": "Record Field for Id"
},
{
"Id": "8719ba9a-7314-5b95-a38e-1acbd0dab6b0",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonTargetField",
"Value": "Id'nin Yazılacağı Kayıt Alanı"
},
{
"Id": "1059eb89-9b04-5524-9272-e40634b3545f",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonIsRequired",
"Value": "Selection required"
},
{
"Id": "05786192-1daf-52ef-90b7-daaf730bcfce",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonIsRequired",
"Value": "Seçim zorunlu"
},
{
"Id": "634f4c35-51bf-5f1c-9668-ad50814d1f2e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonPlaceholder",
"Value": "Select a reject reason"
},
{
"Id": "6de88842-9612-556a-9162-048c6eaee634",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormWorkflow.RejectReasonPlaceholder",
"Value": "Red nedeni seçin"
}
]
}

View file

@ -72,7 +72,7 @@ Yetki ağacı: `App.Administration` grubu › `App.Administration.Definitions`
### 7. Entegrasyonlar
Dış entegrasyon yok. Ülke/Şehir/İlçe, Para Birimi, Dil, Sektör, Kullanıcılar, Departmanlar, Şubeler ve Satınalma tanımları yalnızca okunur. Onaycılar'ı Satınalma › Satınalma Talepleri departman onayı (`ApprovalArea = 'Purchase'`) okuyacaktır; diğer onay yerleri ilgili modüllerin akışları için hazırdır.
Dış entegrasyon yok. Ülke/Şehir/İlçe, Para Birimi, Dil, Sektör, Kullanıcılar, Departmanlar, Şubeler ve Satınalma tanımları yalnızca okunur. Onaycılar'ı Satınalma › Malzeme Talepleri departman onayı (`ApprovalArea = 'Purchase'`) okuyacaktır; diğer onay yerleri ilgili modüllerin akışları için hazırdır.
### 8. Akış örneği
@ -178,7 +178,7 @@ Dış entegrasyon yok. Ülke/Şehir/İlçe, Para Birimi, Dil, Sektör, Kullanıc
| Menü | `App.Administration.Definitions` › `App.Definitions.Approver` (sıra 6) |
| Yetki | `App.Definitions.Approver` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Onaycılar — uygulamadaki tüm onay adımlarında her departman için kimin onaylayacağını tutan tek liste. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Adm_T_Approver` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
@ -190,7 +190,7 @@ Dış entegrasyon yok. Ülke/Şehir/İlçe, Para Birimi, Dil, Sektör, Kullanıc
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `ApprovalArea` | Onay Yeri | dxSelectBox | ✓ | ✓ | liste: Purchase, PurchaseDesk, Warehouse, HumanResources, Finance |
| `ApprovalArea` | Onay Yeri | dxSelectBox | ✓ | ✓ | liste: Purchase, Warehouse, HumanResources, Finance |
| `ApproverUserId` | Onaylayacak Kişi | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers |
| `IsActive` | Aktif | dxCheckBox | ✓ | ✓ | |

View file

@ -67,54 +67,58 @@ Yok. Tanımlar Yönetim › Tanımlamalar › Kontaklar'ın Satış ve Satınalm
- Geri alma: Kontak sekmeleri bu tanımlara bağlı olduğu için önce kontak tarafındaki seçimler temizlenir; ekranlar `delete_wizard` ile silinir, tablolar yalnızca açık onayla `dropTables: true` ile düşürülür.
<!-- module-doc:section App.Prc.MaterialRequests -->
## Satınalma Talepleri (`App.Prc.MaterialRequests`)
## Malzeme Talepleri (`App.Prc.MaterialRequests`)
### 1. Amaç ve kapsam
Departmanların iç ve dış ihtiyaç duydukları malzemeleri satınalmaya iletmesi. Talep eden personel talebi ve satırlarını girer, onaya gönderir; talep önce talep edenin departman onaycısına, sonra satınalmaya (Satınalma Masası) düşer. Onaylanan talep Tüm Talepler listesinde satınalma ekibinin işine düşer; teklif toplama süreci buradan başlar. Teklif, sipariş, transfer ve stok miktarı bu dalın kapsamında değildir.
Departmanların iç ve dış ihtiyaç duydukları malzemeleri satınalmaya iletmesi. Talep eden personel talebi ve satırlarını girer, onaya gönderir; talep önce talep edenin departman onaycısına, sonra satınalmaya düşer. Onaylanan talep Tüm Talepler listesinde satınalma ekibinin işine düşer; teklif toplama süreci buradan başlar. Teklif, sipariş, transfer ve stok miktarı bu dalın kapsamında değildir.
### 2. Analiz ve kararlar
- Wizard List ekranları + menüsüz satır alt formu + Grid Workflow. Beş liste aynı tabloyu (`Prc_T_MaterialRequest`) filtreyle gösterir.
- Wizard List ekranları + menüsüz satır alt formu + Grid Workflow. Taleplerim ve iki onay listesi aynı tabloyu (`Prc_T_MaterialRequest`) filtreyle gösterir; Tüm Talepler başlık × kalem görünümünden (`Prc_V_MaterialRequestLine`) okur, her kalem bir satırdır.
- Belge no numaratörden: `MaterialRequest` (MT-YYYY-00001, yıllık sıfırlanır). Şirket alanı açılmadı (kiracı kapsamı).
- Departman onaycısı: `Adm_T_Approver`'da Onay Yeri = Satınalma (`Purchase`) ve talebin departmanı. Satınalma onaycısı: Onaycılar'a eklenen yeni Onay Yeri **Satınalma Masası** (`PurchaseDesk`) satırları, departmandan bağımsız. Herhangi bir onaycının onayı yeterli.
- Departman onaycısı: `Adm_T_Approver`'da Onay Yeri = Satınalma (`Purchase`) ve talebin departmanı. Satınalma onaycısı Onaycılar'da tanımlanmaz: Satınalma Onayı Bekleyenler ekranında karar verebilen, yani `App.Prc.MaterialRequestPurchaseApproval.Update` yetkisi doğrudan ya da rolüyle verilmiş aktif kullanıcılardır (satınalma ekibinin rolüne bu yetki verilir). Herhangi bir onaycının onayı yeterli.
- Taslak aşaması: kayıt Taslak açılır; akış platformun Onaya Gönder işlemiyle başlar (`WorkflowDto.ApprovalIsManualStart: true` — yoksa yeni kayıt akışı kendiliğinden başlatır ve satır girilemez).
- Görseldeki "Kendi Depomda" ve "Tüm Depolar" stok kolonları üretilmedi: uygulamada stok miktarı/hareket tablosu yok; stok modülü gelince ayrı talep.
- Karşılama Şekli: Satınalma / Depodan Transfer.
- Onay akışının "onaylayan" alanı platformda kullanıcı adı (metin) yazdığı için akışa bağlanmadı (`ApprovalUserFieldName` boş); departman/satınalma onaylayanı kayıt sonrası prosedürde kaydı son güncelleyen kullanıcıdan yazılır. `ApproverId` kolonu kullanılmaz.
- İş akışı kolonları tablo tasarımcısının Workflow şablonundaki dörtlüdür: `ApprovalUserName` nvarchar(256) (onaycı kullanıcı adları), `ApprovalStatus` nvarchar(50) (bulunulan düğümün başlığı), `ApprovalDate` datetime, `ApprovalDescription` nvarchar(200); dört ekranın `WorkflowDto`'su bunlara bağlıdır. Departman ve satınalma kararları ayrı kolonda tutulmaz; her karar (karar veren, açıklama, red nedeni) iş akışı notlarına yazılır.
- Onay ve red bilgileri popup düzenleme formunda yer almaz (iş akışı yazar); listede kolon olarak görünür.
- Red nedeni ayrı kolonda tutulmaz: onay düğümleri red sırasında Red Nedenleri listesinden (`Prc_T_RejectReason`, zorunlu) seçim ister; seçilen neden ve not `ApprovalDescription`'a ve iş akışı notuna yazılır.
- Satır tablosu `Prc_T_MaterialRequestItem` adıyla açıldı (birim kolonu `Adm_T_Uom.Id` ile aynı tipte, nvarchar). İlk açılan `Prc_T_MaterialRequestLine` scripti (UomId uniqueidentifier) kullanılmıyor.
- Talep durumu satırlardan türetilir; ayrı Tamamlandı butonu yerine satır durumu (Bekliyor / Tamamlandı / İptal) kullanılır.
- Planlama aşaması kaldırıldı (Planlama Bekleyenler listesi, satırdaki Planlandı durumu ve Planlanacak Miktar). İşlemde (`InProgress`) durumu korunur: talep depoya girdiğinde kullanılacak, şimdilik hiçbir kural yazmaz.
- Talebin durumu yalnız iş akışından gelir: `ApprovalStatus` boş (taslak) › Departman Onayı › Satınalma Onayı › Onaylandı ya da Reddedildi. Ayrı `Status` kolonu yoktur; karşılama satır durumundan (Bekliyor / Tamamlandı / İptal) izlenir.
- Planlama aşaması kaldırıldı (Planlama Bekleyenler listesi, satırdaki Planlandı durumu ve Planlanacak Miktar).
### 3. Menü ve ekranlar
Satınalma (`App.Prc`) › Satınalma Talepleri (`App.Prc.MaterialRequests`):
Satınalma (`App.Prc`) › Malzeme Talepleri (`App.Prc.MaterialRequests`):
| Ekran | Kod | Filtre / not |
| --- | --- | --- |
| Taleplerim | `App.Prc.MyMaterialRequest` | talep eden = oturumdaki kullanıcı; ekle/sil açık, akış |
| Departman Onayı Bekleyenler | `App.Prc.MaterialRequestDeptApproval` | ApprovalStatus = Departman Onayı; yalnız kendine düşenler |
| Satınalma Onayı Bekleyenler | `App.Prc.MaterialRequestPurchaseApproval` | ApprovalStatus = Satınalma Onayı; yalnız kendine düşenler |
| Tüm Talepler | `App.Prc.MaterialRequest` | filtresiz; ekle/sil açık, akış |
| Tüm Talepler | `App.Prc.MaterialRequest` | filtresiz, kalem kalem; ekle/sil kapalı, yalnız kalemin sorumlusu ve durumu düzenlenir, akış yok |
Talep Satırları (`App.Prc.MaterialRequestLine`) menüsüz alt formdur; beş listenin hepsinde sekmedir. Red Nedenleri (`App.Prc.RejectReason`) Satınalma › Tanımlamalar altındadır (bkz. Tanımlamalar).
Talep Satırları (`App.Prc.MaterialRequestLine`) menüsüz alt formdur; Taleplerim ve iki onay listesinde sekmedir (Tüm Talepler kalemleri kolon olarak gösterir). Red Nedenleri (`App.Prc.RejectReason`) Satınalma › Tanımlamalar altındadır (bkz. Tanımlamalar).
### 4. Veri modeli
Tüm tablolar kiracı + tam audit + soft delete taşır.
- `Prc_T_MaterialRequest`: RequestNo, Priority (0–3), RequesterUserId, DepartmentId, WarehouseId, LocationId, RequestDate, Description, Status (Draft / DeptApproval / PurchaseApproval / Approved / InProgress / PartiallyDone / Done / Rejected), ApprovalStatus, ApprovalDate, ApprovalNote (akış), DeptApproverUserNames / PurchaseApproverUserNames (`|` ayrılmış kullanıcı adları), DeptApprovedBy/Date/Note, PurchaseApprovedBy/Date/Note, RejectReasonId, ApproverId (kullanılmaz).
- `Prc_T_MaterialRequest`: RequestNo, Priority (0–3), RequesterUserId, DepartmentId, WarehouseId, LocationId, RequestDate, Description, ApprovalUserName, ApprovalStatus, ApprovalDate, ApprovalDescription (iş akışı), DeptApproverUserNames / PurchaseApproverUserNames (`|` ayrılmış kullanıcı adları).
- `Prc_T_MaterialRequestItem`: MaterialRequestId (FK, cascade), ProductId, Quantity, UomId (nvarchar, `Adm_T_Uom`), FulfillmentMethod (Purchase / Transfer), DueDate, RequestReason, DeliveredQuantity, PurchaseResponsibleUserId, LineStatus (Waiting / Done / Cancelled).
- `Prc_T_RejectReason`: Name, SortOrder, IsActive.
- Prosedür `Prc_P_MaterialRequestAfterSave(@Id, @UserId)`.
- Görünüm `Prc_V_MaterialRequestLine`: talep başlığı LEFT JOIN silinmemiş kalemler; `Id` kalemin Id'si (kalemi olmayan talepte talebin Id'si), `MaterialRequestId` talep. Tüm Talepler'in kaynağıdır.
- Prosedür `Prc_P_MaterialRequestAfterSave(@Id)`.
### 5. İş kuralları ve hesaplar
- `Prc_P_MaterialRequestAfterSave` talep kaydı/güncellemesi ve satır ekleme/güncelleme/silme sonrasında çalışır: onaycı kullanıcı listelerini Onaycılar'dan yeniden yazar; ApprovalStatus departman adımını geçince departman onay bilgilerini, Onaylandı/Reddedildi olunca satınalma onay bilgilerini (onaylayan, tarih, not) kopyalar; Status'u türetir: Departman Onayı → DeptApproval, Satınalma Onayı → PurchaseApproval, Reddedildi → Rejected, Onaylandı → satırlara göre (tümü Tamamlandı/İptal → Done, bir kısmı Tamamlandı → PartiallyDone, değilse Approved), boş → Draft.
- `Prc_P_MaterialRequestAfterSave` talep kaydı/güncellemesi sonrasında çalışır ve onaycı kullanıcı listelerini (`DeptApproverUserNames`, `PurchaseApproverUserNames`) Onaycılar'dan yeniden yazar. Onaycılar sonradan değişirse talep yeniden kaydedilene kadar liste eski kalır. İki liste formda ve listede gizlidir; iş akışı onaycıyı bu alanlardan okur (`@DeptApproverUserNames`, `@PurchaseApproverUserNames`).
- Onaya gönderilmiş talebe satır eklenemez ve satır silinemez (alt form Insert/Delete Before komutu). Onay sürecindeki ya da onaylanmış talep silinemez; reddedilen talep silinebilir.
- Ürün listesi yalnız aktif ve satın alınabilir ürünleri gösterir; birim ürünün satınalma biriminden (yoksa biriminden) kopyalanır, salt okunurdur.
- Depo değişince konum temizlenir; konum listesi depoya göre süzülür. Talep eden değişince departmanı kullanıcı kartından gelir; yeni kayıtta departman oturumdaki kullanıcının departmanıdır.
- Onaycı listesi boşsa akış o adımda hata verir: Onaycılar'a ilgili departman için Satınalma ve en az bir Satınalma Masası satırı tanımlanmalıdır.
- Tüm Talepler'de düzenleme `UpdateCommand` ile yalnız `Prc_T_MaterialRequestItem`'ın `PurchaseResponsibleUserId` ve `LineStatus` kolonlarını yazar; başlık alanları salt okunurdur. Kartlar kalemi değil talebi sayar (tablodan okur).
- Onaycı listesi boşsa akış o adımda hata verir: Onaycılar'a ilgili departman için Satınalma satırı tanımlanmalı ve en az bir kullanıcıya (ya da rolüne) Satınalma Onayı Bekleyenler güncelleme yetkisi verilmelidir.
### 6. Yetki ve görünürlük
@ -122,26 +126,27 @@ Yetki ağacı: `App.Prc` › `App.Prc.MaterialRequests` › her liste (+ Create/
### 7. Entegrasyonlar
Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma Masası); Envanter › Tanımlamalar › Ürünler, Depolar, Konumlar; İK departmanları; Yönetim › Tanımlamalar › Diziler (`MaterialRequest`). Akış e-posta bildirimi platformun iş akışı altyapısındadır. Teklif/sipariş modülüyle entegrasyon yok.
Yönetim › Tanımlamalar › Onaycılar (onay yeri Satınalma); Yönetim › Roller (Satınalma Onayı Bekleyenler yetkisi); Envanter › Tanımlamalar › Ürünler, Depolar, Konumlar; İK departmanları; Yönetim › Tanımlamalar › Diziler (`MaterialRequest`). Akış e-posta bildirimi platformun iş akışı altyapısındadır. Teklif/sipariş modülüyle entegrasyon yok.
### 8. Akış örneği
1. Onaycılar'da BT departmanı için Satınalma yerinde Ayşe, Satınalma Masası yerinde Mehmet tanımlıdır.
1. Onaycılar'da BT departmanı için Satınalma yerinde Ayşe tanımlıdır; Mehmet'in rolünde Satınalma Onayı Bekleyenler güncelleme yetkisi vardır.
2. BT'den Ali Taleplerim'de yeni talep açar (MT-2026-00001), depo/konum seçer, Talep Satırları'na 10 adet ürün, termin ve neden girer; Onaya Gönder der.
3. Ayşe Departman Onayı Bekleyenler'de talebi onaylar; talep Satınalma Onayı Bekleyenler'e geçer.
4. Mehmet onaylar; talep Onaylandı olur, Ali'ye bilgi gider.
5. Satınalma Tüm Talepler'de satıra sorumluyu atar ve satırı Tamamlandı yapar (talep Tamamlandı).
5. Satınalma Tüm Talepler'de satıra sorumluyu atar ve satırı Tamamlandı yapar.
### 9. Sık sorulanlar
- **Talebim onay listesinde görünmüyor?** Onaycılar'da talebin departmanı için aktif Satınalma satırı olmalı; satınalma adımı için aktif Satınalma Masası satırı gerekir.
- **Talebim onay listesinde görünmüyor?** Onaycılar'da talebin departmanı için aktif Satınalma satırı olmalı; satınalma adımı için en az bir aktif kullanıcının (doğrudan ya da rolüyle) Satınalma Onayı Bekleyenler güncelleme yetkisi olmalıdır. Onaycılar ya da yetkiler talep açıldıktan sonra değiştiyse talebi bir kez kaydedin.
- **Onaya gönderdikten sonra satır ekleyebilir miyim?** Hayır; talep reddedilirse ya da akış başa dönerse yeniden düzenlenir.
- **Kendi Depomda / Tüm Depolar miktarları nerede?** Stok modülü geldiğinde eklenecek.
- **Red nedeni nereden seçilir?** Onay listelerindeki talep formunda Red Nedeni alanından (Satınalma › Tanımlamalar › Red Nedenleri).
- **Red nedeni nereden seçilir?** Reddet düğmesinin açtığı karar penceresinde, Satınalma › Tanımlamalar › Red Nedenleri listesinden; neden ve not Onay Açıklaması kolonunda görünür.
### 10. Dosyalar ve geri alma
- Tablolar: `sql/object/Prc_T_MaterialRequest.sql`, `Prc_T_MaterialRequestItem.sql`, `Prc_T_RejectReason.sql` (+ postgres eşleri); kullanılmayan `Prc_T_MaterialRequestLine.sql`.
- Görünüm: `sql/object/Prc_V_MaterialRequestLine.sql` (+ postgres).
- Prosedür: `sql/object/Prc_P_MaterialRequestAfterSave.sql` (+ postgres).
- Wizard'lar: `Prc_MaterialRequestLine.json`, `Prc_MyMaterialRequest.json`, `Prc_MaterialRequestDeptApproval.json`, `Prc_MaterialRequestPurchaseApproval.json`, `Prc_MaterialRequest.json`, `Prc_RejectReason.json`; güncellenen `Adm_Approver.json`.
- Numaratör: `App.Definitions.Sequence` › `MaterialRequest`.
@ -255,7 +260,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Menü | `App.Prc.Definitions` › `App.Prc.RejectReason` (sıra 5) |
| Yetki | `App.Prc.RejectReason` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Red Nedenleri — satınalma taleplerinin reddedilme sebepleri (ihtiyaç yok, miktar uygun değil, yanlış ürün, bütçe uygun değil, diğer). |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_T_RejectReason` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
@ -270,7 +275,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| `SortOrder` | Sıra | dxNumberBox | ✓ | ✓ | |
| `IsActive` | Aktif | dxCheckBox | ✓ | ✓ | |
### Satınalma Talepleri (`App.Prc.MaterialRequests`)
### Malzeme Talepleri (`App.Prc.MaterialRequests`)
#### Taleplerim — `App.Prc.MyMaterialRequest`
@ -281,7 +286,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Menü | `App.Prc.MaterialRequests` › `App.Prc.MyMaterialRequest` (sıra 1) |
| Yetki | `App.Prc.MyMaterialRequest` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Taleplerim — açtığınız malzeme talepleri, tüm durumlarıyla. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_T_MaterialRequest` · filtre `"IsDeleted" = 'false' AND "RequesterUserId" = '@USERID'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
@ -289,7 +294,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alt formlar | `App.Prc.MaterialRequestLine` (Id → MaterialRequestId) |
| Widget'lar | `App.Prc.MaterialRequest.Widget.Mine`, `App.Prc.MaterialRequest.Widget.Pending`, `App.Prc.MaterialRequest.Widget.Approved`, `App.Prc.MaterialRequest.Widget.Rejected` |
| Komutlar | InsertAfter: `Prc_P_MaterialRequestAfterSave` · UpdateAfter: `Prc_P_MaterialRequestAfterSave` · DeleteBefore: SQL |
| Form varsayılanları | RequestNo=MaterialRequest1, RequesterUserId=@USERID, DepartmentId=, RequestDate=@NOW, Priority=0, Status=Draft |
| Form varsayılanları | RequestNo=MaterialRequest1, RequesterUserId=@USERID, DepartmentId=, RequestDate=@NOW, Priority=0 |
| Onay akışı | Start 'Onaya Gönderildi' → Approval 'Departman Onayı' → Approval 'Satınalma Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
@ -301,21 +306,13 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse · script |
| `LocationId` | Konum | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Location (← WarehouseId) |
| `RequestDate` | Talep Tarihi | dxDateBox | ✓ | ✓ | |
| `Status` | Durum | dxSelectBox | ✓ | ✓ | liste: Draft, DeptApproval, PurchaseApproval, Approved, InProgress, PartiallyDone, Done, Rejected |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `RejectReasonId` | Red Nedeni | dxSelectBox | | ✓ | sorgu: Prc_T_RejectReason |
| `Description` | Genel Açıklama | dxTextBox | | ✓ | |
| `PurchaseApproverUserNames` | Satınalma Onaycıları | dxTextBox | | | |
| `ApproverId` | Onaylayan Kimlik | dxTextBox | | | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | | |
| `ApprovalUserName` | Onaylayan Kullanıcı Adı | dxTextBox | | | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | | |
| `ApprovalNote` | Onay Notu | dxTextArea | | | |
| `DeptApproverUserNames` | Departman Onaycıları | dxTextBox | | ✓ | |
| `PurchaseApprovedBy` | Satınalma Onaylayan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `DeptApprovedBy` | Departman Onaylayan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `PurchaseApprovalDate` | Satınalma Onay Tarihi | dxDateBox | | ✓ | |
| `DeptApprovalDate` | Departman Onay Tarihi | dxDateBox | | ✓ | |
| `PurchaseApprovalNote` | Satınalma Onay Notu | dxTextArea | | ✓ | |
| `DeptApprovalNote` | Departman Onay Notu | dxTextArea | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | | |
| `PurchaseApproverUserNames` | Satınalma Onaycıları | dxTextBox | | | |
| `DeptApproverUserNames` | Departman Onaycıları | dxTextBox | | | |
#### Departman Onayı Bekleyenler — `App.Prc.MaterialRequestDeptApproval`
@ -326,7 +323,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Menü | `App.Prc.MaterialRequests` › `App.Prc.MaterialRequestDeptApproval` (sıra 2) |
| Yetki | `App.Prc.MaterialRequestDeptApproval` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Departman Onayı Bekleyenler — departman onaycısının kararını bekleyen malzeme talepleri. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_T_MaterialRequest` · filtre `"IsDeleted" = 'false' AND "ApprovalStatus" = N'Departman Onayı'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✓ · sil ✗ · popup |
@ -334,7 +331,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alt formlar | `App.Prc.MaterialRequestLine` (Id → MaterialRequestId) |
| Widget'lar | `App.Prc.MaterialRequest.Status.DeptApproval`, `App.Prc.MaterialRequest.Status.PurchaseApproval`, `App.Prc.MaterialRequest.Status.Rejected` |
| Komutlar | UpdateAfter: `Prc_P_MaterialRequestAfterSave` |
| Form varsayılanları | Priority=0, Status=Draft |
| Form varsayılanları | Priority=0 |
| Onay akışı | Start 'Onaya Gönderildi' → Approval 'Departman Onayı' → Approval 'Satınalma Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
@ -346,14 +343,13 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse |
| `LocationId` | Konum | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Location |
| `RequestDate` | Talep Tarihi | dxDateBox | ✓ | ✓ | |
| `Status` | Durum | dxSelectBox | ✓ | ✓ | liste: Draft, DeptApproval, PurchaseApproval, Approved, InProgress, PartiallyDone, Done, Rejected |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `RejectReasonId` | Red Nedeni | dxSelectBox | | ✓ | sorgu: Prc_T_RejectReason |
| `Description` | Genel Açıklama | dxTextArea | | ✓ | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | | |
| `DeptApproverUserNames` | Departman Onaycıları | dxTextBox | | | |
| `PurchaseApproverUserNames` | Satınalma Onaycıları | dxTextBox | | | |
| `ApprovalUserName` | Onaylayan Kullanıcı Adı | dxTextBox | | | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | | |
| `ApprovalNote` | Onay Notu | dxTextArea | | | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | | |
#### Satınalma Onayı Bekleyenler — `App.Prc.MaterialRequestPurchaseApproval`
@ -364,7 +360,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Menü | `App.Prc.MaterialRequests` › `App.Prc.MaterialRequestPurchaseApproval` (sıra 3) |
| Yetki | `App.Prc.MaterialRequestPurchaseApproval` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Satınalma Onayı Bekleyenler — departmanca onaylanmış ve satınalmanın kararını bekleyen malzeme talepleri. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_T_MaterialRequest` · filtre `"IsDeleted" = 'false' AND "ApprovalStatus" = N'Satınalma Onayı'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✗ · düzenle ✓ · sil ✗ · popup |
@ -372,7 +368,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alt formlar | `App.Prc.MaterialRequestLine` (Id → MaterialRequestId) |
| Widget'lar | `App.Prc.MaterialRequest.Status.PurchaseApproval`, `App.Prc.MaterialRequest.Status.Approved`, `App.Prc.MaterialRequest.Status.Rejected` |
| Komutlar | UpdateAfter: `Prc_P_MaterialRequestAfterSave` |
| Form varsayılanları | Priority=0, Status=Draft |
| Form varsayılanları | Priority=0 |
| Onay akışı | Start 'Onaya Gönderildi' → Approval 'Departman Onayı' → Approval 'Satınalma Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
@ -384,17 +380,13 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse |
| `LocationId` | Konum | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Location |
| `RequestDate` | Talep Tarihi | dxDateBox | ✓ | ✓ | |
| `Status` | Durum | dxSelectBox | ✓ | ✓ | liste: Draft, DeptApproval, PurchaseApproval, Approved, InProgress, PartiallyDone, Done, Rejected |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `RejectReasonId` | Red Nedeni | dxSelectBox | | ✓ | sorgu: Prc_T_RejectReason |
| `Description` | Genel Açıklama | dxTextArea | | ✓ | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | | |
| `DeptApproverUserNames` | Departman Onaycıları | dxTextBox | | | |
| `PurchaseApproverUserNames` | Satınalma Onaycıları | dxTextBox | | | |
| `ApprovalUserName` | Onaylayan Kullanıcı Adı | dxTextBox | | | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | | |
| `ApprovalNote` | Onay Notu | dxTextArea | | | |
| `DeptApprovedBy` | Departman Onaylayan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `DeptApprovalDate` | Departman Onay Tarihi | dxDateBox | | ✓ | |
| `DeptApprovalNote` | Departman Onay Notu | dxTextArea | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | | |
#### Tüm Talepler — `App.Prc.MaterialRequest`
@ -404,42 +396,38 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Yol | Liste (Wizard) |
| Menü | `App.Prc.MaterialRequests` › `App.Prc.MaterialRequest` (sıra 5) |
| Yetki | `App.Prc.MaterialRequest` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Tüm Talepler — kurumun bütün malzeme talepleri, tüm durumlarıyla; satınalma yönetiminin listesi. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Kaynak | `Prc_T_MaterialRequest` · filtre `"IsDeleted" = 'false'` |
| Açıklama | Tüm Talepler — kurumun bütün malzeme talepleri, tüm durumlarıyla, kalem kalem; satınalma yönetiminin listesi. |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_V_MaterialRequestLine` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Düzenleme | ekle ✗ · düzenle ✓ · sil ✗ · popup |
| Kapsam | Kiracı |
| Alt formlar | `App.Prc.MaterialRequestLine` (Id → MaterialRequestId) |
| Widget'lar | `App.Prc.MaterialRequest.Status.DeptApproval`, `App.Prc.MaterialRequest.Status.PurchaseApproval`, `App.Prc.MaterialRequest.Status.Approved`, `App.Prc.MaterialRequest.Status.Rejected` |
| Komutlar | InsertAfter: `Prc_P_MaterialRequestAfterSave` · UpdateAfter: `Prc_P_MaterialRequestAfterSave` · DeleteBefore: SQL |
| Form varsayılanları | RequestNo=MaterialRequest1, RequesterUserId=@USERID, DepartmentId=, RequestDate=@NOW, Priority=0, Status=Draft |
| Onay akışı | Start 'Onaya Gönderildi' → Approval 'Departman Onayı' → Approval 'Satınalma Onayı' → End 'Onaylandı' → End 'Reddedildi' |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers · script |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse · script |
| `LocationId` | Konum | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Location (← WarehouseId) |
| `RequestDate` | Talep Tarihi | dxDateBox | ✓ | ✓ | |
| `Status` | Durum | dxSelectBox | ✓ | ✓ | liste: Draft, DeptApproval, PurchaseApproval, Approved, InProgress, PartiallyDone, Done, Rejected |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | ✓ | |
| `RejectReasonId` | Red Nedeni | dxSelectBox | | ✓ | sorgu: Prc_T_RejectReason |
| `Description` | Genel Açıklama | dxTextBox | | ✓ | |
| `PurchaseApproverUserNames` | Satınalma Onaycıları | dxTextBox | | | |
| `ApproverId` | Onaylayan Kimlik | dxTextBox | | | |
| `RequestNo` | Talep No | dxTextBox | | | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | | sorgu: AbpUsers |
| `DepartmentId` | Departman | dxSelectBox | ✓ | | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | | sorgu: Inv_T_Warehouse |
| `LocationId` | Konum | dxSelectBox | ✓ | | sorgu: Inv_T_Location (← WarehouseId) |
| `RequestDate` | Talep Tarihi | dxDateBox | ✓ | | |
| `Description` | Genel Açıklama | dxTextBox | | | |
| `ApprovalStatus` | Onay Durumu | dxTextBox | | | |
| `ApprovalUserName` | Onaylayan Kullanıcı Adı | dxTextBox | | | |
| `ApprovalDate` | Onay Tarihi | dxDateBox | | | |
| `ApprovalNote` | Onay Notu | dxTextArea | | | |
| `DeptApproverUserNames` | Departman Onaycıları | dxTextBox | | ✓ | |
| `PurchaseApprovedBy` | Satınalma Onaylayan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `DeptApprovedBy` | Departman Onaylayan | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `PurchaseApprovalDate` | Satınalma Onay Tarihi | dxDateBox | | ✓ | |
| `DeptApprovalDate` | Departman Onay Tarihi | dxDateBox | | ✓ | |
| `PurchaseApprovalNote` | Satınalma Onay Notu | dxTextArea | | ✓ | |
| `DeptApprovalNote` | Departman Onay Notu | dxTextArea | | ✓ | |
| `ApprovalDescription` | Onay Açıklaması | dxTextBox | | | |
| `MaterialRequestId` | Malzeme Talebi | dxTextBox | | | |
| `ProductId` | Ürün | dxSelectBox | | | sorgu: Inv_T_Product |
| `Quantity` | Miktar | dxNumberBox | | | |
| `UomId` | Birim | dxSelectBox | | | sorgu: Adm_T_Uom |
| `FulfillmentMethod` | Karşılama Şekli | dxSelectBox | | | liste: Purchase, Transfer |
| `DueDate` | Termin Tarihi | dxDateBox | | | |
| `RequestReason` | Talep Nedeni | dxTextArea | | | |
| `DeliveredQuantity` | Teslim | dxNumberBox | | | |
| `PurchaseResponsibleUserId` | Satınalma Sorumlusu | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `LineStatus` | Satır Durumu | dxSelectBox | ✓ | ✓ | liste: Waiting, Done, Cancelled |
#### Talep Satırları — `App.Prc.MaterialRequestLine`
@ -450,12 +438,12 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Menü | Menüsüz (alt form) |
| Yetki | `App.Prc.MaterialRequestLine` + `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
| Açıklama | Talep Satırları — malzeme talebinde istenen ürünler. |
| Geliştirme talebi | Satınalma -> Satınalma Talepleri |
| Geliştirme talebi | Satınalma -> Malzeme Talepleri |
| Kaynak | `Prc_T_MaterialRequestItem` · filtre `"IsDeleted" = 'false'` |
| Görünümler | Grid, Card, Pivot, Chart (açılış grid) |
| Düzenleme | ekle ✓ · düzenle ✓ · sil ✓ · popup |
| Kapsam | Kiracı |
| Komutlar | InsertBefore: SQL · InsertAfter: `Prc_P_MaterialRequestAfterSave` · UpdateAfter: `Prc_P_MaterialRequestAfterSave` · DeleteBefore: SQL · DeleteAfter: `Prc_P_MaterialRequestAfterSave` |
| Komutlar | InsertBefore: SQL · DeleteBefore: SQL |
| Form varsayılanları | FulfillmentMethod=Purchase, DeliveredQuantity=0, LineStatus=Waiting, Quantity=1, PurchaseResponsibleUserId=@USERID |
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
@ -466,7 +454,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| `UomId` | Birim | dxSelectBox | ✓ | ✓ | sorgu: Adm_T_Uom |
| `FulfillmentMethod` | Karşılama Şekli | dxSelectBox | ✓ | ✓ | liste: Purchase, Transfer |
| `DueDate` | Termin Tarihi | dxDateBox | ✓ | ✓ | |
| `RequestReason` | Talep Nedeni | dxTextBox | ✓ | ✓ | |
| `RequestReason` | Talep Nedeni | dxTextArea | ✓ | ✓ | |
| `DeliveredQuantity` | Teslim | dxNumberBox | | ✓ | |
| `PurchaseResponsibleUserId` | Satınalma Sorumlusu | dxSelectBox | | ✓ | sorgu: AbpUsers |
| `LineStatus` | Satır Durumu | dxSelectBox | ✓ | ✓ | liste: Waiting, Done, Cancelled |

View file

@ -1,6 +1,5 @@
CREATE OR REPLACE PROCEDURE public."Prc_P_MaterialRequestAfterSave"(
"p_Id" uuid,
"p_UserId" uuid DEFAULT NULL
"p_Id" uuid
)
LANGUAGE plpgsql
AS $$
@ -17,45 +16,20 @@ BEGIN
AND a."TenantId" IS NOT DISTINCT FROM r."TenantId"),
"PurchaseApproverUserNames" = (
SELECT string_agg(u."UserName", '|')
FROM "Adm_T_Approver" a
INNER JOIN "AbpUsers" u ON u."Id" = a."ApproverUserId" AND u."IsDeleted" = false
WHERE a."IsDeleted" = false AND a."IsActive" = true AND a."ApprovalArea" = 'PurchaseDesk'
AND a."TenantId" IS NOT DISTINCT FROM r."TenantId")
WHERE r."Id" = "p_Id";
UPDATE "Prc_T_MaterialRequest" r SET
"PurchaseApprovedBy" = COALESCE("p_UserId", r."LastModifierId"),
"PurchaseApprovalDate" = COALESCE(r."ApprovalDate", now()),
"PurchaseApprovalNote" = r."ApprovalNote"
WHERE r."Id" = "p_Id" AND r."PurchaseApprovedBy" IS NULL AND r."DeptApprovedBy" IS NOT NULL
AND r."ApprovalStatus" IN ('Onaylandı', 'Reddedildi');
UPDATE "Prc_T_MaterialRequest" r SET
"DeptApprovedBy" = COALESCE("p_UserId", r."LastModifierId"),
"DeptApprovalDate" = COALESCE(r."ApprovalDate", now()),
"DeptApprovalNote" = r."ApprovalNote"
WHERE r."Id" = "p_Id" AND r."DeptApprovedBy" IS NULL
AND r."ApprovalStatus" IN ('Satınalma Onayı', 'Onaylandı', 'Reddedildi');
UPDATE "Prc_T_MaterialRequest" r SET "Status" =
CASE
WHEN r."ApprovalStatus" = 'Departman Onayı' THEN 'DeptApproval'
WHEN r."ApprovalStatus" = 'Satınalma Onayı' THEN 'PurchaseApproval'
WHEN r."ApprovalStatus" = 'Reddedildi' THEN 'Rejected'
WHEN r."ApprovalStatus" = 'Onaylandı' THEN
CASE
WHEN s."Total" > 0 AND s."Closed" = s."Total" THEN 'Done'
WHEN s."Closed" > 0 THEN 'PartiallyDone'
ELSE 'Approved'
END
ELSE 'Draft'
END
FROM (
SELECT COUNT(*) AS "Total",
COALESCE(SUM(CASE WHEN l."LineStatus" IN ('Done', 'Cancelled') THEN 1 ELSE 0 END), 0) AS "Closed"
FROM "Prc_T_MaterialRequestItem" l
WHERE l."MaterialRequestId" = "p_Id" AND l."IsDeleted" = false
) s
FROM "AbpUsers" u
WHERE u."IsDeleted" = false AND u."IsActive" = true
AND u."TenantId" IS NOT DISTINCT FROM r."TenantId"
AND EXISTS (
SELECT 1
FROM "AbpPermissionGrants" g
WHERE g."Name" = 'App.Prc.MaterialRequestPurchaseApproval.Update'
AND g."TenantId" IS NOT DISTINCT FROM r."TenantId"
AND ((g."ProviderName" = 'U' AND g."ProviderKey" = u."Id"::text)
OR (g."ProviderName" = 'R' AND g."ProviderKey" IN (
SELECT ro."Name"
FROM "AbpUserRoles" ur
INNER JOIN "AbpRoles" ro ON ro."Id" = ur."RoleId"
WHERE ur."UserId" = u."Id")))))
WHERE r."Id" = "p_Id";
END;
$$;

View file

@ -10,20 +10,12 @@ CREATE TABLE IF NOT EXISTS public."Prc_T_MaterialRequest"
"LocationId" uuid NOT NULL,
"RequestDate" timestamp NOT NULL DEFAULT LOCALTIMESTAMP,
"Description" text NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Draft',
"ApprovalStatus" varchar(32) NULL,
"ApproverId" uuid NULL,
"ApprovalUserName" varchar(256) NULL,
"ApprovalStatus" varchar(50) NULL,
"ApprovalDate" timestamp NULL,
"ApprovalNote" text NULL,
"ApprovalDescription" varchar(200) NULL,
"DeptApproverUserNames" text NULL,
"PurchaseApproverUserNames" text NULL,
"DeptApprovedBy" uuid NULL,
"DeptApprovalDate" timestamp NULL,
"DeptApprovalNote" text NULL,
"PurchaseApprovedBy" uuid NULL,
"PurchaseApprovalDate" timestamp NULL,
"PurchaseApprovalNote" text NULL,
"RejectReasonId" uuid NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
@ -34,6 +26,6 @@ CREATE TABLE IF NOT EXISTS public."Prc_T_MaterialRequest"
CONSTRAINT "PK_Prc_T_MaterialRequest" PRIMARY KEY ("Id")
);
CREATE INDEX IF NOT EXISTS "IX_Prc_T_MaterialRequest_TenantId_Status" ON public."Prc_T_MaterialRequest" ("TenantId", "Status");
CREATE INDEX IF NOT EXISTS "IX_Prc_T_MaterialRequest_TenantId_ApprovalStatus" ON public."Prc_T_MaterialRequest" ("TenantId", "ApprovalStatus");
CREATE INDEX IF NOT EXISTS "IX_Prc_T_MaterialRequest_TenantId_RequesterUserId" ON public."Prc_T_MaterialRequest" ("TenantId", "RequesterUserId");

View file

@ -0,0 +1,11 @@
CREATE OR REPLACE VIEW public."Prc_V_MaterialRequestLine"
AS
-- Tüm Talepler listesinin kaynağı: talep başlığı × kalem, her kalem bir satır. Kalemi olmayan talep de
-- listeden düşmesin diye LEFT JOIN; o satırın anahtarı talebin kendi Id'sidir.
SELECT COALESCE(i."Id", r."Id") AS "Id", r."Id" AS "MaterialRequestId", r."TenantId", r."IsDeleted",
r."RequestNo", r."Priority", r."RequesterUserId", r."DepartmentId", r."WarehouseId", r."LocationId", r."RequestDate",
r."Description", r."ApprovalStatus", r."ApprovalUserName", r."ApprovalDate", r."ApprovalDescription",
i."ProductId", i."Quantity", i."UomId", i."FulfillmentMethod", i."DueDate", i."RequestReason", i."DeliveredQuantity",
i."PurchaseResponsibleUserId", i."LineStatus"
FROM "Prc_T_MaterialRequest" r
LEFT JOIN "Prc_T_MaterialRequestItem" i ON i."MaterialRequestId" = r."Id" AND i."IsDeleted" = FALSE;

View file

@ -1,12 +1,13 @@
CREATE OR ALTER PROCEDURE [dbo].[Prc_P_MaterialRequestAfterSave]
@Id uniqueidentifier,
@UserId uniqueidentifier = NULL
@Id uniqueidentifier
AS
BEGIN
SET NOCOUNT ON;
IF @Id IS NULL RETURN;
-- Onaycılar (Adm_T_Approver): departman onayı = Purchase + talebin departmanı, satınalma onayı = PurchaseDesk
-- Departman onaycıları: Onaycılar'da talebin departmanı için Satınalma onay yeri.
-- Satınalma onaycıları: Satınalma Onayı Bekleyenler ekranında karar verebilen (güncelleme yetkisi
-- doğrudan ya da rolüyle verilmiş) aktif kullanıcılar.
UPDATE r SET
DeptApproverUserNames = (
SELECT STRING_AGG(u.UserName, '|')
@ -17,52 +18,21 @@ BEGIN
AND ((a.TenantId IS NULL AND r.TenantId IS NULL) OR a.TenantId = r.TenantId)),
PurchaseApproverUserNames = (
SELECT STRING_AGG(u.UserName, '|')
FROM Adm_T_Approver a
INNER JOIN AbpUsers u ON u.Id = a.ApproverUserId AND u.IsDeleted = 0
WHERE a.IsDeleted = 0 AND a.IsActive = 1 AND a.ApprovalArea = 'PurchaseDesk'
AND ((a.TenantId IS NULL AND r.TenantId IS NULL) OR a.TenantId = r.TenantId))
FROM AbpUsers u
WHERE u.IsDeleted = 0 AND u.IsActive = 1
AND ((u.TenantId IS NULL AND r.TenantId IS NULL) OR u.TenantId = r.TenantId)
AND EXISTS (
SELECT 1
FROM AbpPermissionGrants g
WHERE g.Name = N'App.Prc.MaterialRequestPurchaseApproval.Update'
AND ((g.TenantId IS NULL AND r.TenantId IS NULL) OR g.TenantId = r.TenantId)
AND ((g.ProviderName = N'U' AND g.ProviderKey = CAST(u.Id AS nvarchar(64)))
OR (g.ProviderName = N'R' AND g.ProviderKey IN (
SELECT ro.Name
FROM AbpUserRoles ur
INNER JOIN AbpRoles ro ON ro.Id = ur.RoleId
WHERE ur.UserId = u.Id)))))
FROM Prc_T_MaterialRequest r
WHERE r.Id = @Id;
-- Satınalma kararı: departman adımı daha önce kaydedilmişse
UPDATE r SET
PurchaseApprovedBy = COALESCE(@UserId, r.LastModifierId),
PurchaseApprovalDate = COALESCE(r.ApprovalDate, GETDATE()),
PurchaseApprovalNote = r.ApprovalNote
FROM Prc_T_MaterialRequest r
WHERE r.Id = @Id AND r.PurchaseApprovedBy IS NULL AND r.DeptApprovedBy IS NOT NULL
AND r.ApprovalStatus IN (N'Onaylandı', N'Reddedildi');
-- Departman kararı (onaylanıp satınalmaya geçti ya da departmanda reddedildi)
UPDATE r SET
DeptApprovedBy = COALESCE(@UserId, r.LastModifierId),
DeptApprovalDate = COALESCE(r.ApprovalDate, GETDATE()),
DeptApprovalNote = r.ApprovalNote
FROM Prc_T_MaterialRequest r
WHERE r.Id = @Id AND r.DeptApprovedBy IS NULL
AND r.ApprovalStatus IN (N'Satınalma Onayı', N'Onaylandı', N'Reddedildi');
-- Durum: onay akışından ve satır durumlarından türetilir
UPDATE r SET Status =
CASE
WHEN r.ApprovalStatus = N'Departman Onayı' THEN 'DeptApproval'
WHEN r.ApprovalStatus = N'Satınalma Onayı' THEN 'PurchaseApproval'
WHEN r.ApprovalStatus = N'Reddedildi' THEN 'Rejected'
WHEN r.ApprovalStatus = N'Onaylandı' THEN
CASE
WHEN s.Total > 0 AND s.Closed = s.Total THEN 'Done'
WHEN s.Closed > 0 THEN 'PartiallyDone'
ELSE 'Approved'
END
ELSE 'Draft'
END
FROM Prc_T_MaterialRequest r
CROSS APPLY (
SELECT COUNT(*) AS Total,
ISNULL(SUM(CASE WHEN l.LineStatus IN ('Done', 'Cancelled') THEN 1 ELSE 0 END), 0) AS Closed
FROM Prc_T_MaterialRequestItem l
WHERE l.MaterialRequestId = r.Id AND l.IsDeleted = 0
) s
WHERE r.Id = @Id;
END
GO

View file

@ -12,20 +12,12 @@ BEGIN
[LocationId] uniqueidentifier NOT NULL,
[RequestDate] datetime NOT NULL DEFAULT GETDATE(),
[Description] nvarchar(MAX) NULL,
[Status] nvarchar(32) NOT NULL DEFAULT 'Draft',
[ApprovalStatus] nvarchar(32) NULL,
[ApproverId] uniqueidentifier NULL,
[ApprovalUserName] nvarchar(256) NULL,
[ApprovalStatus] nvarchar(50) NULL,
[ApprovalDate] datetime NULL,
[ApprovalNote] nvarchar(MAX) NULL,
[ApprovalDescription] nvarchar(200) NULL,
[DeptApproverUserNames] nvarchar(MAX) NULL,
[PurchaseApproverUserNames] nvarchar(MAX) NULL,
[DeptApprovedBy] uniqueidentifier NULL,
[DeptApprovalDate] datetime NULL,
[DeptApprovalNote] nvarchar(MAX) NULL,
[PurchaseApprovedBy] uniqueidentifier NULL,
[PurchaseApprovalDate] datetime NULL,
[PurchaseApprovalNote] nvarchar(MAX) NULL,
[RejectReasonId] uniqueidentifier NULL,
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
[CreatorId] uniqueidentifier NULL,
[LastModificationTime] datetime2 NULL,
@ -41,9 +33,9 @@ BEGIN
END
GO
IF INDEXPROPERTY(OBJECT_ID(N'[dbo].[Prc_T_MaterialRequest]'), 'IX_Prc_T_MaterialRequest_TenantId_Status', 'IndexID') IS NULL
IF INDEXPROPERTY(OBJECT_ID(N'[dbo].[Prc_T_MaterialRequest]'), 'IX_Prc_T_MaterialRequest_TenantId_ApprovalStatus', 'IndexID') IS NULL
BEGIN
CREATE INDEX [IX_Prc_T_MaterialRequest_TenantId_Status] ON [dbo].[Prc_T_MaterialRequest] ([TenantId] ASC, [Status] ASC);
CREATE INDEX [IX_Prc_T_MaterialRequest_TenantId_ApprovalStatus] ON [dbo].[Prc_T_MaterialRequest] ([TenantId] ASC, [ApprovalStatus] ASC);
END
GO

View file

@ -0,0 +1,12 @@
CREATE OR ALTER VIEW [dbo].[Prc_V_MaterialRequestLine]
AS
-- Tüm Talepler listesinin kaynağı: talep başlığı × kalem, her kalem bir satır. Kalemi olmayan talep de
-- listeden düşmesin diye LEFT JOIN; o satırın anahtarı talebin kendi Id'sidir.
SELECT COALESCE(i.Id, r.Id) AS Id, r.Id AS MaterialRequestId, r.TenantId, r.IsDeleted,
r.RequestNo, r.Priority, r.RequesterUserId, r.DepartmentId, r.WarehouseId, r.LocationId, r.RequestDate,
r.Description, r.ApprovalStatus, r.ApprovalUserName, r.ApprovalDate, r.ApprovalDescription,
i.ProductId, i.Quantity, i.UomId, i.FulfillmentMethod, i.DueDate, i.RequestReason, i.DeliveredQuantity,
i.PurchaseResponsibleUserId, i.LineStatus
FROM Prc_T_MaterialRequest r
LEFT JOIN Prc_T_MaterialRequestItem i ON i.MaterialRequestId = r.Id AND i.IsDeleted = 0;
GO

View file

@ -32,19 +32,14 @@
"MenuTr": "Onaycılar",
"TitleEn": "Approvers",
"TitleTr": "Onaycılar",
"DescEn": "Approvers — the single list of who approves what for each department across the application.\nWhere: Administration › Definitions › Approvers.\nFields: department (shown as Branch - Department), approval area (Purchasing / Purchasing Desk / Warehouse / Human Resources / Finance), approver user, active.\nUsed by: approval workflows read the approvers of the record's department and area. Purchasing › Material Requests: the department approval uses area Purchasing for the requester's department; the purchasing approval uses area Purchasing Desk (any department). Several approvers may be defined for the same department and area; any one of them can approve. Passive rows are ignored by workflows.",
"DescTr": "Onaycılar — uygulamadaki tüm onay adımlarında her departman için kimin onaylayacağını tutan tek liste.\nNerede: Yönetim › Tanımlamalar › Onaycılar.\nAlanlar: departman (Şube - Departman olarak gösterilir), onay yeri (Satınalma / Satınalma Masası / Depo / İnsan Kaynakları / Finans), onaylayacak kişi, aktif.\nKullanıldığı yer: onay akışları kaydın departmanı ve onay yerine göre onaycıları buradan okur. Satınalma › Satınalma Talepleri: departman onayı talep edenin departmanındaki Satınalma satırlarını, satınalma onayı ise Satınalma Masası satırlarını (departmandan bağımsız) kullanır. Aynı departman ve onay yerine birden çok onaycı girilebilir; herhangi birinin onayı yeterlidir. Pasif satırlar akışlarda kullanılmaz.",
"DescEn": "Approvers — the single list of who approves what for each department across the application.\nWhere: Administration › Definitions › Approvers.\nFields: department (shown as Branch - Department), approval area (Purchasing / Warehouse / Human Resources / Finance), approver user, active.\nUsed by: approval workflows read the approvers of the record's department and area. Purchasing › Material Requests: the department approval uses area Purchasing for the requester's department; the purchasing approval is not defined here: it goes to users with update permission on Awaiting Purchasing Approval (directly or through their role). Several approvers may be defined for the same department and area; any one of them can approve. Passive rows are ignored by workflows.",
"DescTr": "Onaycılar — uygulamadaki tüm onay adımlarında her departman için kimin onaylayacağını tutan tek liste.\nNerede: Yönetim › Tanımlamalar › Onaycılar.\nAlanlar: departman (Şube - Departman olarak gösterilir), onay yeri (Satınalma / Depo / İnsan Kaynakları / Finans), onaylayacak kişi, aktif.\nKullanıldığı yer: onay akışları kaydın departmanı ve onay yerine göre onaycıları buradan okur. Satınalma › Malzeme Talepleri: departman onayı talep edenin departmanındaki Satınalma satırlarını, satınalma onayı burada tanımlanmaz: Satınalma Onayı Bekleyenler ekranında güncelleme yetkisi olan kullanıcılar (doğrudan ya da rolüyle) onaylar. Aynı departman ve onay yerine birden çok onaycı girilebilir; herhangi birinin onayı yeterlidir. Pasif satırlar akışlarda kullanılmaz.",
"Texts": [
{
"Key": "App.StaticLookup.Purchase",
"En": "Purchasing",
"Tr": "Satınalma"
},
{
"Key": "App.StaticLookup.PurchaseDesk",
"En": "Purchasing Desk",
"Tr": "Satınalma Masası"
},
{
"Key": "App.StaticLookup.Warehouse",
"En": "Warehouse",
@ -1061,7 +1056,7 @@
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": "[{\"key\":\"Purchase\",\"name\":\"App.StaticLookup.Purchase\"},{\"key\":\"PurchaseDesk\",\"name\":\"App.StaticLookup.PurchaseDesk\"},{\"key\":\"Warehouse\",\"name\":\"App.StaticLookup.Warehouse\"},{\"key\":\"HumanResources\",\"name\":\"App.StaticLookup.HumanResources\"},{\"key\":\"Finance\",\"name\":\"App.StaticLookup.Finance\"}]"
"LookupQuery": "[{\"key\":\"Purchase\",\"name\":\"App.StaticLookup.Purchase\"},{\"key\":\"Warehouse\",\"name\":\"App.StaticLookup.Warehouse\"},{\"key\":\"HumanResources\",\"name\":\"App.StaticLookup.HumanResources\"},{\"key\":\"Finance\",\"name\":\"App.StaticLookup.Finance\"}]"
},
"ValidationRuleDto": [
{
@ -1403,7 +1398,6 @@
"App.Definitions.Approver.Title",
"App.Definitions.Approver.Desc",
"App.StaticLookup.Purchase",
"App.StaticLookup.PurchaseDesk",
"App.StaticLookup.Warehouse",
"App.StaticLookup.HumanResources",
"App.StaticLookup.Finance",
@ -1428,6 +1422,6 @@
},
"DevRequest": {
"Id": "53294e77-a4fb-45e4-a80e-acf07b5d75be",
"Title": "Satınalma -> Satınalma Talepleri"
"Title": "Satınalma -> Malzeme Talepleri"
}
}

File diff suppressed because it is too large Load diff

View file

@ -25,15 +25,15 @@
},
"Language": {
"MenuParentEn": "Material Requests",
"MenuParentTr": "Satınalma Talepleri",
"MenuParentTr": "Malzeme Talepleri",
"PermissionGroupEn": "Purchasing",
"PermissionGroupTr": "Satınalma",
"MenuEn": "Awaiting Department Approval",
"MenuTr": "Departman Onayı Bekleyenler",
"TitleEn": "Awaiting Department Approval",
"TitleTr": "Departman Onayı Bekleyenler",
"DescEn": "Awaiting Department Approval — material requests waiting for the department approver's decision.\nWhere: Purchasing › Material Requests › Awaiting Department Approval.\nWho: approvers defined in Administration › Definitions › Approvers with area Purchasing for the requester's department; each approver sees only the requests assigned to them.\nWhat to do: open the request, review the Request Lines tab, then Approve (the request moves to purchasing approval) or Reject with a note and a rejection reason (Purchasing › Definitions › Rejection Reasons). Lines cannot be added or removed here.\nRelated: My Requests, Awaiting Purchasing Approval, All Requests.",
"DescTr": "Departman Onayı Bekleyenler — departman onaycısının kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Satınalma Talepleri › Departman Onayı Bekleyenler.\nKim: Yönetim › Tanımlamalar › Onaycılar'da talep edenin departmanı için Satınalma onay yerinde tanımlı onaycılar; her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi açıp Talep Satırları sekmesini inceleyin, ardından Onayla (talep satınalma onayına geçer) ya da not ve red nedeniyle (Satınalma › Tanımlamalar › Red Nedenleri) Reddet. Burada satır eklenip silinemez.\nİlişkili: Taleplerim, Satınalma Onayı Bekleyenler, Tüm Talepler.",
"DescEn": "Awaiting Department Approval — material requests waiting for the department approver's decision.\nWhere: Purchasing › Material Requests › Awaiting Department Approval.\nWho: approvers defined in Administration › Definitions › Approvers with area Purchasing for the requester's department; each approver sees only the requests assigned to them.\nWhat to do: open the request, review the Request Lines tab, then Approve (the request moves to purchasing approval) or Reject by choosing a rejection reason (Purchasing › Definitions › Rejection Reasons) and an optional note; the reason and note are written to the approval description. Lines cannot be added or removed here.\nRelated: My Requests, Awaiting Purchasing Approval, All Requests.",
"DescTr": "Departman Onayı Bekleyenler — departman onaycısının kararını bekleyen malzeme talepleri.\nNerede: Satınalma › Malzeme Talepleri › Departman Onayı Bekleyenler.\nKim: Yönetim › Tanımlamalar › Onaycılar'da talep edenin departmanı için Satınalma onay yerinde tanımlı onaycılar; her onaycı yalnız kendisine düşen talepleri görür.\nNe yapılır: talebi açıp Talep Satırları sekmesini inceleyin, ardından Onayla (talep satınalma onayına geçer) ya da red nedeni (Satınalma › Tanımlamalar › Red Nedenleri) seçip isteğe bağlı notla Reddet; neden ve not onay açıklamasına yazılır. Burada satır eklenip silinemez.\nİlişkili: Taleplerim, Satınalma Onayı Bekleyenler, Tüm Talepler.",
"Texts": [
{
"Key": "App.Prc.MaterialRequest.Group.Request",
@ -45,11 +45,6 @@
"En": "Approval / Rejection",
"Tr": "Onay / Red Bilgileri"
},
{
"Key": "App.Prc.MaterialRequest.Status.Draft",
"En": "Draft",
"Tr": "Taslak"
},
{
"Key": "App.Prc.MaterialRequest.Status.DeptApproval",
"En": "Department Approval",
@ -65,21 +60,6 @@
"En": "Approved",
"Tr": "Onaylandı"
},
{
"Key": "App.Prc.MaterialRequest.Status.InProgress",
"En": "In Progress",
"Tr": "İşlemde"
},
{
"Key": "App.Prc.MaterialRequest.Status.PartiallyDone",
"En": "Partially Done",
"Tr": "Kısmi Tamamlandı"
},
{
"Key": "App.Prc.MaterialRequest.Status.Done",
"En": "Done",
"Tr": "Tamamlandı"
},
{
"Key": "App.Prc.MaterialRequest.Status.Rejected",
"En": "Rejected",
@ -139,10 +119,10 @@
"Tr": "Talep Tarihi"
},
{
"FieldName": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"En": "Status",
"Tr": "Durum"
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.GeneralDescription",
"En": "General Description",
"Tr": "Genel Açıklama"
},
{
"FieldName": "ApprovalStatus",
@ -150,18 +130,6 @@
"En": "Approval Status",
"Tr": "Onay Durumu"
},
{
"FieldName": "RejectReasonId",
"CaptionName": "App.Listform.ListformField.RejectReasonId",
"En": "Rejection Reason",
"Tr": "Red Nedeni"
},
{
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.GeneralDescription",
"En": "General Description",
"Tr": "Genel Açıklama"
},
{
"FieldName": "DeptApproverUserNames",
"CaptionName": "App.Listform.ListformField.DeptApproverUserNames",
@ -174,6 +142,12 @@
"En": "Purchasing Approvers",
"Tr": "Satınalma Onaycıları"
},
{
"FieldName": "ApprovalUserName",
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
"En": "Approval User Name",
"Tr": "Onaylayan Kullanıcı Adı"
},
{
"FieldName": "ApprovalDate",
"CaptionName": "App.Listform.ListformField.ApprovalDate",
@ -181,10 +155,10 @@
"Tr": "Onay Tarihi"
},
{
"FieldName": "ApprovalNote",
"CaptionName": "App.Listform.ListformField.ApprovalNote",
"En": "Approval Note",
"Tr": "Onay Notu"
"FieldName": "ApprovalDescription",
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
"En": "Approval Description",
"Tr": "Onay Açıklaması"
}
]
}
@ -257,7 +231,7 @@
],
"UpdateCommand": null,
"UpdateBeforeCommand": null,
"UpdateAfterCommand": "DECLARE @_id uniqueidentifier = (SELECT TOP 1 Id FROM Prc_T_MaterialRequest WHERE Id IN @Id); EXEC dbo.Prc_P_MaterialRequestAfterSave @_id, @LastModifierId",
"UpdateAfterCommand": "DECLARE @_id uniqueidentifier = (SELECT TOP 1 Id FROM Prc_T_MaterialRequest WHERE Id IN @Id); EXEC dbo.Prc_P_MaterialRequestAfterSave @_id",
"UpdateFieldsDefaultValueDto": [
{
"FieldName": "LastModificationTime",
@ -281,20 +255,13 @@
"Value": "0",
"CustomValueType": 1,
"SqlQuery": null
},
{
"FieldName": "Status",
"FieldDbType": 16,
"Value": "Draft",
"CustomValueType": 1,
"SqlQuery": null
}
],
"WidgetsDto": [
{
"ColGap": 3,
"ColSpan": 4,
"SqlQuery": "SELECT 'App.Prc.MaterialRequest.Status.DeptApproval' AS \"Title\", CAST(COUNT(*) AS VARCHAR(32)) AS \"Value\", 'orange' AS \"Color\", 'App.Prc.MaterialRequestDeptApproval' AS \"SubTitle\", 'FaUserTie' AS \"Icon\" FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"Status\" = 'DeptApproval' UNION ALL SELECT 'App.Prc.MaterialRequest.Status.PurchaseApproval', CAST(COUNT(*) AS VARCHAR(32)), 'purple', 'App.Prc.MaterialRequestDeptApproval', 'FaHourglassHalf' FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"Status\" = 'PurchaseApproval' UNION ALL SELECT 'App.Prc.MaterialRequest.Status.Rejected', CAST(COUNT(*) AS VARCHAR(32)), 'red', 'App.Prc.MaterialRequestDeptApproval', 'FaTimesCircle' FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"Status\" = 'Rejected'",
"SqlQuery": "SELECT 'App.Prc.MaterialRequest.Status.DeptApproval' AS \"Title\", CAST(COUNT(*) AS VARCHAR(32)) AS \"Value\", 'orange' AS \"Color\", 'App.Prc.MaterialRequestDeptApproval' AS \"SubTitle\", 'FaUserTie' AS \"Icon\" FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"ApprovalStatus\" = N'Departman Onayı' UNION ALL SELECT 'App.Prc.MaterialRequest.Status.PurchaseApproval', CAST(COUNT(*) AS VARCHAR(32)), 'purple', 'App.Prc.MaterialRequestDeptApproval', 'FaHourglassHalf' FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"ApprovalStatus\" = N'Satınalma Onayı' UNION ALL SELECT 'App.Prc.MaterialRequest.Status.Rejected', CAST(COUNT(*) AS VARCHAR(32)), 'red', 'App.Prc.MaterialRequestDeptApproval', 'FaTimesCircle' FROM \"Prc_T_MaterialRequest\" WHERE \"TenantId\" = '@TENANTID' AND \"ApprovalStatus\" = N'Reddedildi'",
"Title": "Title",
"Value": "Value",
"ValueClassName": "text-3xl",
@ -569,13 +536,13 @@
"DefaultLayout": "grid"
},
"WorkflowDto": {
"ApprovalUserFieldName": null,
"ApprovalUserFieldName": "ApprovalUserName",
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "ApprovalStatus",
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalDescriptionFieldName": "ApprovalDescription",
"ApprovalIsFilterUserName": true,
"ApprovalIsResetWorkflow": true,
"ApprovalIsManualStart": false,
"ApprovalIsManualStart": true,
"Criteria": [
{
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
@ -611,6 +578,13 @@
"PositionX": 40,
"PositionY": 160,
"CompareOutcomes": [],
"RejectReason": {
"TableName": "Prc_T_RejectReason",
"ValueField": "Id",
"TextField": "Name",
"TargetField": "",
"Required": true
},
"Id": "App.Prc.MaterialRequestDeptApproval-N2"
},
{
@ -629,6 +603,13 @@
"PositionX": 40,
"PositionY": 280,
"CompareOutcomes": [],
"RejectReason": {
"TableName": "Prc_T_RejectReason",
"ValueField": "Id",
"TextField": "Name",
"TargetField": "",
"Required": true
},
"Id": "App.Prc.MaterialRequestDeptApproval-N3"
},
{
@ -2024,8 +2005,8 @@
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.GeneralDescription",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
@ -2082,28 +2063,16 @@
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 1,
"ValueExpr": "key",
"DisplayExpr": "name",
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": "[{\"key\":\"Draft\",\"name\":\"App.Prc.MaterialRequest.Status.Draft\"},{\"key\":\"DeptApproval\",\"name\":\"App.Prc.MaterialRequest.Status.DeptApproval\"},{\"key\":\"PurchaseApproval\",\"name\":\"App.Prc.MaterialRequest.Status.PurchaseApproval\"},{\"key\":\"Approved\",\"name\":\"App.Prc.MaterialRequest.Status.Approved\"},{\"key\":\"InProgress\",\"name\":\"App.Prc.MaterialRequest.Status.InProgress\"},{\"key\":\"PartiallyDone\",\"name\":\"App.Prc.MaterialRequest.Status.PartiallyDone\"},{\"key\":\"Done\",\"name\":\"App.Prc.MaterialRequest.Status.Done\"},{\"key\":\"Rejected\",\"name\":\"App.Prc.MaterialRequest.Status.Rejected\"}]"
"LookupQuery": null
},
"ValidationRuleDto": [
{
"Type": "required",
"Message": null,
"IgnoreEmptyValue": false,
"Reevaluate": true,
"ComparisonType": "",
"Pattern": "",
"Max": null,
"Min": null,
"Trim": true
}
],
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
@ -2133,8 +2102,8 @@
"Format": null,
"EditOrderNo": 8,
"EditGroupOrderNo": 1,
"EditorType2": "dxSelectBox",
"ColSpan": 1,
"EditorType2": "dxTextArea",
"ColSpan": 4,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,
"TagBoxOptions": null,
@ -2257,8 +2226,8 @@
},
"Alignment": "left",
"Format": null,
"EditOrderNo": 9,
"EditGroupOrderNo": 1,
"EditOrderNo": null,
"EditGroupOrderNo": null,
"EditorType2": "dxTextBox",
"ColSpan": 1,
"EditorOptions": "{\"readOnly\":true}",
@ -2272,258 +2241,6 @@
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "RejectReasonId",
"CaptionName": "App.Listform.ListformField.RejectReasonId",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 11,
"SourceDbType": 9,
"CultureName": "en",
"DataType": "string",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 2,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM Prc_T_RejectReason WHERE IsDeleted = 'false' AND IsActive = 1 AND TenantId = '@TENANTID' ORDER BY SortOrder, Name"
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Prc.MaterialRequestDeptApproval.Create",
"R": "App.Prc.MaterialRequestDeptApproval",
"U": "App.Prc.MaterialRequestDeptApproval.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": "left",
"Format": null,
"EditOrderNo": 10,
"EditGroupOrderNo": 1,
"EditorType2": "dxSelectBox",
"ColSpan": 3,
"EditorOptions": "{\"showClearButton\":true}",
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.GeneralDescription",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 12,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": null
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Prc.MaterialRequestDeptApproval.Create",
"R": "App.Prc.MaterialRequestDeptApproval",
"U": "App.Prc.MaterialRequestDeptApproval.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": "left",
"Format": null,
"EditOrderNo": 11,
"EditGroupOrderNo": 1,
"EditorType2": "dxTextArea",
"ColSpan": 4,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
@ -2547,7 +2264,7 @@
"Visible": false,
"IsActive": true,
"Width": 0,
"ListOrderNo": 13,
"ListOrderNo": 11,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
@ -2673,7 +2390,133 @@
"Visible": false,
"IsActive": true,
"Width": 0,
"ListOrderNo": 14,
"ListOrderNo": 12,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": null
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Prc.MaterialRequestDeptApproval.Create",
"R": "App.Prc.MaterialRequestDeptApproval",
"U": "App.Prc.MaterialRequestDeptApproval.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
"EditorType2": "dxTextBox",
"ColSpan": 1,
"EditorOptions": null,
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "ApprovalUserName",
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 13,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
@ -2796,10 +2639,10 @@
"CaptionName": "App.Listform.ListformField.ApprovalDate",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 15,
"ListOrderNo": 14,
"SourceDbType": 6,
"CultureName": "en",
"DataType": "datetime",
@ -2918,14 +2761,14 @@
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "ApprovalNote",
"CaptionName": "App.Listform.ListformField.ApprovalNote",
"FieldName": "ApprovalDescription",
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 16,
"ListOrderNo": 15,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
@ -3015,7 +2858,7 @@
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
"EditorType2": "dxTextArea",
"EditorType2": "dxTextBox",
"ColSpan": 1,
"EditorOptions": null,
"GridBoxOptions": null,
@ -3035,7 +2878,9 @@
"LanguageKeys": [
"App.Prc.MaterialRequestDeptApproval",
"App.Prc.MaterialRequestDeptApproval.Title",
"App.Prc.MaterialRequestDeptApproval.Desc"
"App.Prc.MaterialRequestDeptApproval.Desc",
"App.Listform.ListformField.ApprovalUserName",
"App.Listform.ListformField.ApprovalDescription"
],
"PermissionGroupNames": [],
"PermissionNames": [
@ -3055,6 +2900,6 @@
},
"DevRequest": {
"Id": "53294e77-a4fb-45e4-a80e-acf07b5d75be",
"Title": "Satınalma -> Satınalma Talepleri"
"Title": "Satınalma -> Malzeme Talepleri"
}
}

View file

@ -26,15 +26,15 @@
},
"Language": {
"MenuParentEn": "Material Requests",
"MenuParentTr": "Satınalma Talepleri",
"MenuParentTr": "Malzeme Talepleri",
"PermissionGroupEn": "Purchasing",
"PermissionGroupTr": "Satınalma",
"MenuEn": "Request Lines",
"MenuTr": "Talep Satırları",
"TitleEn": "Request Lines",
"TitleTr": "Talep Satırları",
"DescEn": "Request Lines — the products requested in a material request.\nWhere: Purchasing › Material Requests › any list › Request Lines tab (no menu of its own).\nFields: product (active, purchasable products), quantity, unit (copied from the product's purchase unit, read-only), fulfilment (Purchasing / Warehouse Transfer), due date, request reason, delivered quantity (read-only), purchasing responsible, line status (Waiting / Done / Cancelled).\nRules: lines can be added or deleted only while the request has not been sent for approval. Changing line statuses recalculates the request status (Partially Done / Done).",
"DescTr": "Talep Satırları — malzeme talebinde istenen ürünler.\nNerede: Satınalma › Satınalma Talepleri › herhangi bir liste › Talep Satırları sekmesi (kendi menüsü yoktur).\nAlanlar: ürün (aktif ve satın alınabilir ürünler), miktar, birim (ürünün satınalma biriminden gelir, salt okunur), karşılama şekli (Satınalma / Depodan Transfer), termin tarihi, talep nedeni, teslim edilen miktar (salt okunur), satınalma sorumlusu, satır durumu (Bekliyor / Tamamlandı / İptal).\nKurallar: satır yalnız talep onaya gönderilmeden eklenip silinebilir. Satır durumları değişince talebin durumu yeniden hesaplanır (Kısmi Tamamlandı / Tamamlandı).",
"DescEn": "Request Lines — the products requested in a material request.\nWhere: Purchasing › Material Requests › My Requests, Awaiting Department Approval and Awaiting Purchasing Approval › Request Lines tab; All Requests shows the lines as columns (no menu of its own).\nFields: product (active, purchasable products), quantity, unit (copied from the product's purchase unit, read-only), fulfilment (Purchasing / Warehouse Transfer), due date, request reason, delivered quantity (read-only), purchasing responsible, line status (Waiting / Done / Cancelled).\nRules: lines can be added or deleted only while the request has not been sent for approval. Changing line statuses recalculates the request status (Partially Done / Done).",
"DescTr": "Talep Satırları — malzeme talebinde istenen ürünler.\nNerede: Satınalma › Malzeme Talepleri › Taleplerim, Departman Onayı Bekleyenler ve Satınalma Onayı Bekleyenler › Talep Satırları sekmesi; Tüm Talepler satırları kolon olarak gösterir (kendi menüsü yoktur).\nAlanlar: ürün (aktif ve satın alınabilir ürünler), miktar, birim (ürünün satınalma biriminden gelir, salt okunur), karşılama şekli (Satınalma / Depodan Transfer), termin tarihi, talep nedeni, teslim edilen miktar (salt okunur), satınalma sorumlusu, satır durumu (Bekliyor / Tamamlandı / İptal).\nKurallar: satır yalnız talep onaya gönderilmeden eklenip silinebilir. Satır durumları değişince talebin durumu yeniden hesaplanır (Kısmi Tamamlandı / Tamamlandı).",
"Texts": [
{
"Key": "App.StaticLookup.Purchase",
@ -151,7 +151,7 @@
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Prc_T_MaterialRequestItem\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": "IF EXISTS (SELECT 1 FROM Prc_T_MaterialRequestItem i INNER JOIN Prc_T_MaterialRequest r ON r.Id = i.MaterialRequestId WHERE i.Id IN @Id AND ISNULL(r.ApprovalStatus, N'') <> N'') THROW 50002, N'Onaya gönderilmiş talepten satır silinemez.', 1;",
"DeleteAfterCommand": "DECLARE @_r uniqueidentifier = (SELECT TOP 1 MaterialRequestId FROM Prc_T_MaterialRequestItem WHERE Id IN @Id); EXEC dbo.Prc_P_MaterialRequestAfterSave @_r, @DeleterId",
"DeleteAfterCommand": null,
"DeleteFieldsDefaultValueDto": [
{
"FieldName": "DeleterId",
@ -170,7 +170,7 @@
],
"InsertCommand": null,
"InsertBeforeCommand": "IF EXISTS (SELECT 1 FROM Prc_T_MaterialRequest WHERE Id = @MaterialRequestId AND ISNULL(ApprovalStatus, N'') <> N'') THROW 50001, N'Onaya gönderilmiş talebe satır eklenemez.', 1;",
"InsertAfterCommand": "EXEC dbo.Prc_P_MaterialRequestAfterSave @MaterialRequestId, @CreatorId",
"InsertAfterCommand": null,
"InsertFieldsDefaultValueDto": [
{
"FieldName": "CreationTime",
@ -203,7 +203,7 @@
],
"UpdateCommand": null,
"UpdateBeforeCommand": null,
"UpdateAfterCommand": "DECLARE @_r uniqueidentifier = (SELECT TOP 1 MaterialRequestId FROM Prc_T_MaterialRequestItem WHERE Id IN @Id); EXEC dbo.Prc_P_MaterialRequestAfterSave @_r, @LastModifierId",
"UpdateAfterCommand": null,
"UpdateFieldsDefaultValueDto": [
{
"FieldName": "LastModificationTime",
@ -513,6 +513,7 @@
"ApprovalDescriptionFieldName": null,
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"ApprovalIsManualStart": false,
"Criteria": []
},
"UserId": null,
@ -920,7 +921,7 @@
"CaptionName": "App.Listform.ListformField.MaterialRequestId",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"Visible": false,
"IsActive": true,
"Width": 0,
"ListOrderNo": 2,
@ -1841,7 +1842,7 @@
"Format": null,
"EditOrderNo": 6,
"EditGroupOrderNo": 1,
"EditorType2": "dxTextBox",
"EditorType2": "dxTextArea",
"ColSpan": 2,
"EditorOptions": "{\"showClearButton\":true,\"maxLength\":256}",
"GridBoxOptions": null,
@ -2279,6 +2280,6 @@
},
"DevRequest": {
"Id": "53294e77-a4fb-45e4-a80e-acf07b5d75be",
"Title": "Satınalma -> Satınalma Talepleri"
"Title": "Satınalma -> Malzeme Talepleri"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@
"TitleEn": "Rejection Reasons",
"TitleTr": "Red Nedenleri",
"DescEn": "Rejection Reasons — reasons a purchasing request can be rejected for (no need, quantity not suitable, wrong product, budget not suitable, other).\nWhere: Purchasing › Definitions › Rejection Reasons.\nFields: reason, sort order (order in the selection list), active.\nUsed by: Purchasing › Material Requests — the department approver or purchasing selects a reason when rejecting a request. Only active reasons are offered; deactivate a used reason instead of deleting it.",
"DescTr": "Red Nedenleri — satınalma taleplerinin reddedilme sebepleri (ihtiyaç yok, miktar uygun değil, yanlış ürün, bütçe uygun değil, diğer).\nNerede: Satınalma › Tanımlamalar › Red Nedenleri.\nAlanlar: red nedeni, sıra (seçim listesindeki sırası), aktif.\nKullanıldığı yer: Satınalma › Satınalma Talepleri — departman onaycısı ya da satınalma talebi reddederken neden seçer. Yalnız aktif nedenler listelenir; kullanılmış neden silinmez, pasife alınır."
"DescTr": "Red Nedenleri — satınalma taleplerinin reddedilme sebepleri (ihtiyaç yok, miktar uygun değil, yanlış ürün, bütçe uygun değil, diğer).\nNerede: Satınalma › Tanımlamalar › Red Nedenleri.\nAlanlar: red nedeni, sıra (seçim listesindeki sırası), aktif.\nKullanıldığı yer: Satınalma › Malzeme Talepleri — departman onaycısı ya da satınalma talebi reddederken neden seçer. Yalnız aktif nedenler listelenir; kullanılmış neden silinmez, pasife alınır."
},
"Groups": [
{
@ -1251,6 +1251,6 @@
},
"DevRequest": {
"Id": "53294e77-a4fb-45e4-a80e-acf07b5d75be",
"Title": "Satınalma -> Satınalma Talepleri"
"Title": "Satınalma -> Malzeme Talepleri"
}
}

View file

@ -72,6 +72,7 @@ export interface ListFormWizardDto {
allowDeleting: boolean
confirmDelete: boolean
allowDetail: boolean
openDetailAfterInsert: boolean
/**
* Ekran verisi `data/{ListFormCode}.json` seed dosyasına aynalanır; migrate satırları geri yükler.
* Wizard adımlarında sorulmaz (MCP ile demo veri taşıyan ekranlar açar), düzenlemede korunur.

View file

@ -490,6 +490,7 @@ export interface GridEditingDto {
allowAllDeleting: boolean
allowAdding: boolean
allowDuplicate: boolean
openDetailAfterInsert: boolean
useIcons: boolean
confirmDelete: boolean
newRowPosition?: NewRowPosition
@ -982,6 +983,16 @@ export interface ListFormWorkflowCriteriaDto {
positionX: number
positionY: number
compareOutcomes: CompareOutcomeDto[]
rejectReason?: WorkflowRejectReasonDto | null
}
/** Onay düğümünde reddin listeden seçilen nedeni; tablo boşsa red yalnızca açıklama ister. */
export interface WorkflowRejectReasonDto {
tableName: string
valueField: string
textField: string
targetField?: string | null
required: boolean
}
export interface LayoutDto {

View file

@ -237,3 +237,9 @@
// },
},
}
/** Kaydın form sayfası adresi: görüntüleme (`formView`) ya da düzenleme (`formEdit`). */
export const getFormRecordUrl = (listFormCode: string, id: unknown, edit = false) =>
(edit ? ROUTES_ENUM.protected.admin.formEdit : ROUTES_ENUM.protected.admin.formView)
.replace(':listFormCode', encodeURIComponent(listFormCode))
.replace(':id', encodeURIComponent(String(id)))

View file

@ -1,4 +1,7 @@
import apiService from './api.service'
import type { WorkflowRejectReasonDto } from '@/proxy/form/models'
export type { WorkflowRejectReasonDto }
export interface WorkflowConditionDto {
compareColumn: string
@ -12,6 +15,11 @@ export interface CompareOutcomeDto {
conditions: WorkflowConditionDto[]
}
export interface WorkflowRejectReasonItemDto {
id: string
text: string
}
export interface WorkflowCriteriaDto {
id: string
listFormCode: string
@ -30,6 +38,7 @@ export interface WorkflowCriteriaDto {
positionX: number
positionY: number
compareOutcomes: CompareOutcomeDto[]
rejectReason?: WorkflowRejectReasonDto | null
}
export interface WorkflowCriteriasDto {
@ -117,11 +126,22 @@ export const workflowService = {
approved: boolean,
note?: string,
currentNodeId?: string,
rejectReasonId?: string,
) {
const response = await apiService.fetchData<WorkflowRunResultDto>({
method: 'POST',
url: `${baseUrl}/decision`,
data: { listFormCode, keys, approved, note, currentNodeId },
data: { listFormCode, keys, approved, note, currentNodeId, rejectReasonId },
})
return response.data
},
async getRejectReasons(listFormCode: string, criteriaId: string) {
const response = await apiService.fetchData<WorkflowRejectReasonItemDto[]>({
method: 'GET',
url: `${baseUrl}/reject-reasons`,
params: { listFormCode, criteriaId },
})
return response.data

View file

@ -5,6 +5,7 @@ import type {
SaveCriteriaInput,
WorkflowConditionDto,
WorkflowCriteriaDto,
WorkflowRejectReasonDto,
} from '@/services/workflow.service'
export type WorkflowCriteriaForm = Partial<WorkflowCriteriaDto> & {
@ -470,6 +471,22 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
positionX: Number(item.positionX || 32),
positionY: Number(item.positionY || 150),
compareOutcomes,
rejectReason: item.kind === 'Approval' ? normalizeRejectReason(item.rejectReason) : null,
}
}
/** Tablo, Id ve metin kolonu birlikte seçilmedikçe tanım saklanmaz; red yalnızca açıklama ister. */
function normalizeRejectReason(
value?: WorkflowRejectReasonDto | null,
): WorkflowRejectReasonDto | null {
if (!value?.tableName || !value.valueField || !value.textField) return null
return {
tableName: value.tableName,
valueField: value.valueField,
textField: value.textField,
targetField: value.targetField || '',
required: value.required !== false,
}
}

View file

@ -5,7 +5,8 @@ import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Field, FieldProps, Form, Formik } from 'formik'
import { useEffect, useState } from 'react'
import { lazy, Suspense, useEffect, useState } from 'react'
import { FaTable } from 'react-icons/fa'
import * as Yup from 'yup'
import { FormEditProps } from './FormEdit'
import { dbSourceTypeOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from './options'
@ -15,6 +16,8 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import CreatableSelect from 'react-select/creatable'
const SqlTableDesignerDialog = lazy(() => import('@/views/developerKit/SqlTableDesignerDialog'))
const schema = Yup.object().shape({
isOrganizationUnit: Yup.bool(),
isTenant: Yup.bool(),
@ -133,6 +136,21 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dbObjects])
// ── Table Designer ──────────────────────────────────────────────────────────
const [designTable, setDesignTable] = useState<{ schemaName: string; tableName: string } | null>(
null,
)
const findSelectedTable = (selectCommand?: string, selectCommandType?: SelectCommandTypeEnum) =>
selectCommandType === SelectCommandTypeEnum.Table && selectCommand
? dbObjects?.tables.find(
(t) =>
t.tableName === selectCommand ||
t.fullName === selectCommand ||
`${t.schemaName}.${t.tableName}` === selectCommand,
)
: undefined
if (!initialValues) {
return null
}
@ -160,7 +178,9 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
checked={field.value === true}
onChange={(checked) => form.setFieldValue(field.name, checked)}
>
{translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)}
{translate(
`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`,
)}
</Checkbox>
)}
</Field>
@ -262,7 +282,10 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
},
]
: []
const selectedTable = findSelectedTable(field.value, values.selectCommandType)
return (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<Select
isClearable
componentAs={CreatableSelect}
@ -273,11 +296,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
placeholder={
isLoadingDbObjects
? translate('::App.Platform.Loading')
: translate(
'::App.ListFormEdit.DatabaseDataSourceSelectCommand',
)
: translate('::App.ListFormEdit.DatabaseDataSourceSelectCommand')
}
value={
field.value ? { label: field.value, value: field.value } : null
}
value={field.value ? { label: field.value, value: field.value } : null}
onChange={(option: any) => {
if (!option) {
form.setFieldValue(field.name, '')
@ -294,7 +317,11 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
form.setFieldValue('selectCommandType', type)
form.setFieldValue('keyFieldName', '')
form.setFieldValue('keyFieldDbSourceType', undefined)
if (!option.__isNew__ && option.__schema != null && option.__rawName) {
if (
!option.__isNew__ &&
option.__schema != null &&
option.__rawName
) {
loadColumns(
values.dataSourceCode ?? '',
option.__schema,
@ -312,6 +339,23 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
setSelectCommandColumns([])
}}
/>
</div>
<Button
type="button"
size="sm"
icon={<FaTable />}
disabled={!values.dataSourceCode || !selectedTable}
onClick={() =>
selectedTable &&
setDesignTable({
schemaName: selectedTable.schemaName,
tableName: selectedTable.tableName,
})
}
>
{translate('::App.ListFormEdit.DesignTable')}
</Button>
</div>
)
}}
</Field>
@ -343,9 +387,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
<span className="text-xs ml-2 text-gray-400">
{translate('::App.ListFormEdit.ColumnsLoadAfterSelect', {
command: translate(
'::App.ListFormEdit.DatabaseDataSourceSelectCommand',
),
command: translate('::App.ListFormEdit.DatabaseDataSourceSelectCommand'),
})}
</span>
) : null
@ -418,7 +460,9 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
checked={field.value === true}
onChange={(checked) => form.setFieldValue(field.name, checked)}
>
{translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)}
{translate(
`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`,
)}
</Checkbox>
)}
</Field>
@ -428,12 +472,29 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
</Card>
<Button block size="sm" variant="solid" loading={isSubmitting}>
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
{isSubmitting
? translate('::App.Platform.Saving')
: translate('::App.Platform.Save')}
</Button>
</FormContainer>
</Form>
)}
</Formik>
{designTable && (
<Suspense fallback={null}>
<SqlTableDesignerDialog
isOpen
dataSource={currentDataSource}
initialTableData={designTable}
onClose={() => setDesignTable(null)}
onDeployed={async (table) => {
await loadDbObjects(currentDataSource)
await loadColumns(currentDataSource, table.schemaName || 'dbo', table.tableName)
}}
/>
</Suspense>
)}
</Container>
)
}

View file

@ -1,16 +1,11 @@
import { Container } from '@/components/shared'
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import type { GridEditingDto, GridEditingPopupDto } from '@/proxy/form/models'
import { useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import type {
GridsEditMode,
GridsEditRefreshMode,
NewRowPosition,
StartEditAction,
} from 'devextreme/common/grids'
import { Field, FieldProps, Form, Formik } from 'formik'
import { object } from 'yup'
import { Field, FieldProps, Form, Formik, getIn } from 'formik'
import type { ReactNode } from 'react'
import { FormEditProps } from './FormEdit'
import {
editingOptionModeOptions,
@ -19,7 +14,11 @@ import {
editingOptionStartEditActionOptions,
} from './options'
const schema = object()
// `editingOptionDto` altındaki alan yolu; popup alanları `popup.` önekiyle.
type EditingField =
| Exclude<keyof GridEditingDto, 'popup'>
| `popup.${keyof GridEditingPopupDto}`
type SelectOption = { value: string; label: string }
function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
const { translate } = useLocalization()
@ -33,467 +32,163 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
<Container>
<Formik
initialValues={listFormValues}
validationSchema={schema}
onSubmit={async (values, formikHelpers) => {
await props.onSubmit(ListFormEditTabs.Edit.EditForm, values, formikHelpers)
}}
>
{({ touched, errors, values, isSubmitting }) => (
{({ touched, errors, values, isSubmitting }) => {
const fieldItem = (
field: EditingField,
labelKey: string,
render: (name: string, label: string) => ReactNode,
) => {
const name = `editingOptionDto.${field}`
const label = translate(labelKey)
return (
<FormItem
key={name}
label={label}
invalid={!!getIn(errors, name) && !!getIn(touched, name)}
errorMessage={getIn(errors, name)}
>
{render(name, label)}
</FormItem>
)
}
const checkbox = (field: EditingField, labelKey: string) =>
fieldItem(field, labelKey, (name, label) => (
<Field name={name} placeholder={label} component={Checkbox} />
))
const input = (field: EditingField, labelKey: string, type: 'text' | 'number') =>
fieldItem(field, labelKey, (name, label) => (
<Field
className={type === 'number' ? 'w-20' : undefined}
type={type}
autoComplete="off"
name={name}
placeholder={label}
component={Input}
/>
))
const text = (field: EditingField, labelKey: string) => input(field, labelKey, 'text')
const number = (field: EditingField, labelKey: string) =>
input(field, labelKey, 'number')
const select = (field: EditingField, labelKey: string, options: SelectOption[]) =>
fieldItem(field, labelKey, (name, label) => (
<Field name={name} placeholder={label}>
{({ field: formikField, form }: FieldProps) => (
<Select
field={formikField}
form={form}
isClearable={true}
options={options}
value={options.filter(
(option) => option.value === getIn(values, name),
)}
onChange={(option) => form.setFieldValue(formikField.name, option?.value)}
/>
)}
</Field>
))
return (
<Form>
<FormContainer size="sm">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Card className="my-2 col-span-2" header="General">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<FormItem
label={translate('::App.ListFormSchedulerOptions.AllowAdding')}
invalid={
errors.editingOptionDto?.allowAdding &&
touched.editingOptionDto?.allowAdding
}
errorMessage={errors.editingOptionDto?.allowAdding}
<div className="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-3 content-start">
<Card
className="my-2"
header={translate('::App.ListFormEdit.EditingGeneral')}
>
<Field
name="editingOptionDto.allowAdding"
placeholder={translate('::App.ListFormSchedulerOptions.AllowAdding')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAddPageUrl')}
invalid={
errors.editingOptionDto?.addPageUrl &&
touched.editingOptionDto?.addPageUrl
}
errorMessage={errors.editingOptionDto?.addPageUrl}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.addPageUrl"
placeholder={translate('::App.ListFormEdit.EditingAddPageUrl')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAddDialogName')}
invalid={
errors.editingOptionDto?.addDialogName &&
touched.editingOptionDto?.addDialogName
}
errorMessage={errors.editingOptionDto?.addDialogName}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.addDialogName"
placeholder={translate('::App.ListFormEdit.EditingAddDialogName')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAddDialogParameters')}
invalid={
errors.editingOptionDto?.addDialogParameters &&
touched.editingOptionDto?.addDialogParameters
}
errorMessage={errors.editingOptionDto?.addDialogParameters}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.addDialogParameters"
placeholder={translate('::App.ListFormEdit.EditingAddDialogParameters')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.AllowUpdating')}
invalid={
errors.editingOptionDto?.allowUpdating &&
touched.editingOptionDto?.allowUpdating
}
errorMessage={errors.editingOptionDto?.allowUpdating}
>
<Field
name="editingOptionDto.allowUpdating"
placeholder={translate('::App.ListFormEdit.AllowUpdating')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate(
'::App.ListFormEdit.SendOnlyChangedFormValuesUpdate',
{select('mode', '::App.ListFormEdit.EditingMode', editingOptionModeOptions)}
{select(
'refreshMode',
'::App.ListFormEdit.EditingRefreshMode',
editingOptionRefreshModeOptions,
)}
invalid={
errors.editingOptionDto?.sendOnlyChangedFormValuesUpdate &&
touched.editingOptionDto?.sendOnlyChangedFormValuesUpdate
}
errorMessage={errors.editingOptionDto?.sendOnlyChangedFormValuesUpdate}
>
<Field
name="editingOptionDto.sendOnlyChangedFormValuesUpdate"
placeholder={translate(
'::App.ListFormEdit.SendOnlyChangedFormValuesUpdate',
{select(
'startEditAction',
'::App.ListFormEdit.EditingStartEditAction',
editingOptionStartEditActionOptions,
)}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAllowDetail')}
invalid={
errors.editingOptionDto?.allowDetail &&
touched.editingOptionDto?.allowDetail
}
errorMessage={errors.editingOptionDto?.allowDetail}
>
<Field
name="editingOptionDto.allowDetail"
placeholder={translate('::App.ListFormEdit.EditingAllowDetail')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAllowDuplicate')}
invalid={
errors.editingOptionDto?.allowDuplicate &&
touched.editingOptionDto?.allowDuplicate
}
errorMessage={errors.editingOptionDto?.allowDuplicate}
>
<Field
name="editingOptionDto.allowDuplicate"
placeholder={translate('::App.ListFormEdit.EditingAllowDuplicate')}
component={Checkbox}
/>
</FormItem>
</div>
<div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<FormItem
label={translate('::App.ListFormEdit.EditingConfirmDelete')}
invalid={
errors.editingOptionDto?.confirmDelete &&
touched.editingOptionDto?.confirmDelete
}
errorMessage={errors.editingOptionDto?.confirmDelete}
>
<Field
name="editingOptionDto.confirmDelete"
placeholder={translate('::App.ListFormEdit.EditingConfirmDelete')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAllowDeleting')}
invalid={
errors.editingOptionDto?.allowDeleting &&
touched.editingOptionDto?.allowDeleting
}
errorMessage={errors.editingOptionDto?.allowDeleting}
>
<Field
name="editingOptionDto.allowDeleting"
placeholder={translate('::App.ListFormEdit.EditingAllowDeleting')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAllowDeleteAll')}
invalid={
errors.editingOptionDto?.allowAllDeleting &&
touched.editingOptionDto?.allowAllDeleting
}
errorMessage={errors.editingOptionDto?.allowAllDeleting}
>
<Field
name="editingOptionDto.allowAllDeleting"
placeholder={translate('::App.ListFormEdit.EditingAllowDeleteAll')}
component={Checkbox}
/>
</FormItem>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<FormItem
label={translate('::App.ListFormEdit.EditingUseIcons')}
invalid={
errors.editingOptionDto?.useIcons && touched.editingOptionDto?.useIcons
}
errorMessage={errors.editingOptionDto?.useIcons}
>
<Field
name="editingOptionDto.useIcons"
placeholder={translate('::App.ListFormEdit.EditingUseIcons')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingSelectTextOnEditStart')}
invalid={
errors.editingOptionDto?.selectTextOnEditStart &&
touched.editingOptionDto?.selectTextOnEditStart
}
errorMessage={errors.editingOptionDto?.selectTextOnEditStart}
>
<Field
name="editingOptionDto.selectTextOnEditStart"
placeholder={translate(
{text('editColumnName', '::App.ListFormEdit.EditingEditColumnName')}
<div className="grid grid-cols-2 gap-3">
{checkbox('useIcons', '::App.ListFormEdit.EditingUseIcons')}
{checkbox(
'selectTextOnEditStart',
'::App.ListFormEdit.EditingSelectTextOnEditStart',
)}
component={Checkbox}
/>
</FormItem>
</div>
<FormItem
label={translate('::App.ListFormEdit.EditingMode')}
invalid={errors.editingOptionDto?.mode && touched.editingOptionDto?.mode}
errorMessage={errors.editingOptionDto?.mode}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.mode"
placeholder={translate('::App.Platform.Mode')}
>
{({ field, form }: FieldProps<GridsEditMode>) => (
<Select
field={field}
form={form}
isClearable={true}
options={editingOptionModeOptions}
value={editingOptionModeOptions?.filter(
(option) => option.value === values.editingOptionDto.mode,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingRefreshMode')}
invalid={
errors.editingOptionDto?.refreshMode &&
touched.editingOptionDto?.refreshMode
}
errorMessage={errors.editingOptionDto?.refreshMode}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.refreshMode"
placeholder={translate('::App.ListFormEdit.EditingRefreshMode')}
>
{({ field, form }: FieldProps<GridsEditRefreshMode>) => (
<Select
field={field}
form={form}
isClearable={true}
options={editingOptionRefreshModeOptions}
value={editingOptionRefreshModeOptions?.filter(
(option) => option.value === values.editingOptionDto.refreshMode,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingNewRowPosition')}
invalid={
errors.editingOptionDto?.newRowPosition &&
touched.editingOptionDto?.newRowPosition
}
errorMessage={errors.editingOptionDto?.newRowPosition}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.newRowPosition"
placeholder={translate('::App.ListFormEdit.EditingNewRowPosition')}
>
{({ field, form }: FieldProps<NewRowPosition>) => (
<Select
field={field}
form={form}
isClearable={true}
options={editingOptionNewRowPositionOptions}
value={editingOptionNewRowPositionOptions?.filter(
(option) => option.value === values.editingOptionDto.newRowPosition,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingStartEditAction')}
invalid={
errors.editingOptionDto?.startEditAction &&
touched.editingOptionDto?.startEditAction
}
errorMessage={errors.editingOptionDto?.startEditAction}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.startEditAction"
placeholder={translate('::App.ListFormEdit.EditingStartEditAction')}
>
{({ field, form }: FieldProps<StartEditAction>) => (
<Select
field={field}
form={form}
isClearable={true}
options={editingOptionStartEditActionOptions}
value={editingOptionStartEditActionOptions?.filter(
(option) =>
option.value === values.editingOptionDto.startEditAction,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingEditColumnName')}
invalid={
errors.editingOptionDto?.editColumnName &&
touched.editingOptionDto?.editColumnName
}
errorMessage={errors.editingOptionDto?.editColumnName}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.editColumnName"
placeholder={translate('::App.ListFormEdit.EditingEditColumnName')}
component={Input}
/>
</FormItem>
</div>
</div>
</Card>
<Card className="my-2" header={translate('::App.Platform.New')}>
<div className="grid grid-cols-3 gap-3">
{checkbox('allowAdding', '::App.ListFormSchedulerOptions.AllowAdding')}
{checkbox(
'openDetailAfterInsert',
'::App.ListFormEdit.EditingOpenDetailAfterInsert',
)}
{checkbox('allowDuplicate', '::App.ListFormEdit.EditingAllowDuplicate')}
</div>
{select(
'newRowPosition',
'::App.ListFormEdit.EditingNewRowPosition',
editingOptionNewRowPositionOptions,
)}
{text('addPageUrl', '::App.ListFormEdit.EditingAddPageUrl')}
{text('addDialogName', '::App.ListFormEdit.EditingAddDialogName')}
{text(
'addDialogParameters',
'::App.ListFormEdit.EditingAddDialogParameters',
)}
</Card>
<Card className="my-2" header={translate('::App.Platform.Edit')}>
<div className="grid grid-cols-3 gap-3">
{checkbox('allowUpdating', '::App.ListFormEdit.AllowUpdating')}
{checkbox('allowDetail', '::App.ListFormEdit.EditingAllowDetail')}
{checkbox(
'sendOnlyChangedFormValuesUpdate',
'::App.ListFormEdit.SendOnlyChangedFormValuesUpdate',
)}
</div>
</Card>
<Card className="my-2" header={translate('::App.Platform.Delete')}>
<div className="grid grid-cols-3 gap-3">
{checkbox('allowDeleting', '::App.ListFormEdit.EditingAllowDeleting')}
{checkbox('confirmDelete', '::App.ListFormEdit.EditingConfirmDelete')}
{checkbox('allowAllDeleting', '::App.ListFormEdit.EditingAllowDeleteAll')}
</div>
</Card>
</div>
<Card className="my-2" header="Popup">
{values.editingOptionDto.mode === 'popup' && (
<>
<FormItem
label={translate('::App.ListFormEdit.EditingShowTitle')}
invalid={
errors.editingOptionDto?.popup?.showTitle &&
touched.editingOptionDto?.popup?.showTitle
}
errorMessage={errors.editingOptionDto?.popup?.showTitle}
>
<Field
name="editingOptionDto.popup.showTitle"
placeholder={translate('::App.ListFormEdit.EditingShowTitle')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.Listform.ListformField.Title')}
invalid={
errors.editingOptionDto?.popup?.title &&
touched.editingOptionDto?.popup?.title
}
errorMessage={errors.editingOptionDto?.popup?.title}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.popup.title"
placeholder={translate('::App.Listform.ListformField.Title')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.DetailsWidth')}
invalid={
errors.editingOptionDto?.popup?.width &&
touched.editingOptionDto?.popup?.width
}
errorMessage={errors.editingOptionDto?.popup?.width}
>
<Field
className="w-20"
type="number"
autoComplete="off"
name="editingOptionDto.popup.width"
placeholder={translate('::App.ListFormEdit.DetailsWidth')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.DetailsHeight')}
invalid={
errors.editingOptionDto?.popup?.height &&
touched.editingOptionDto?.popup?.height
}
errorMessage={errors.editingOptionDto?.popup?.height}
>
<Field
className="w-20"
type="number"
autoComplete="off"
name="editingOptionDto.popup.height"
placeholder={translate('::App.ListFormEdit.DetailsHeight')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.Platform.Fullscreen')}
invalid={
errors.editingOptionDto?.popup?.fullScreen &&
touched.editingOptionDto?.popup?.fullScreen
}
errorMessage={errors.editingOptionDto?.popup?.fullScreen}
>
<Field
name="editingOptionDto.popup.fullScreen"
placeholder={translate('::App.Platform.Fullscreen')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.ResizeEnabled')}
invalid={
errors.editingOptionDto?.popup?.resizeEnabled &&
touched.editingOptionDto?.popup?.resizeEnabled
}
errorMessage={errors.editingOptionDto?.popup?.resizeEnabled}
>
<Field
name="editingOptionDto.popup.resizeEnabled"
placeholder={translate('::App.ListFormEdit.ResizeEnabled')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingHideOnOutsideClick')}
invalid={
errors.editingOptionDto?.popup?.hideOnOutsideClick &&
touched.editingOptionDto?.popup?.hideOnOutsideClick
}
errorMessage={errors.editingOptionDto?.popup?.hideOnOutsideClick}
>
<Field
name="editingOptionDto.popup.hideOnOutsideClick"
placeholder={translate(
{checkbox('popup.showTitle', '::App.ListFormEdit.EditingShowTitle')}
{text('popup.title', '::App.Listform.ListformField.Title')}
{number('popup.width', '::App.ListFormEdit.DetailsWidth')}
{number('popup.height', '::App.ListFormEdit.DetailsHeight')}
{checkbox('popup.fullScreen', '::App.Platform.Fullscreen')}
{checkbox('popup.resizeEnabled', '::App.ListFormEdit.ResizeEnabled')}
{checkbox(
'popup.hideOnOutsideClick',
'::App.ListFormEdit.EditingHideOnOutsideClick',
)}
component={Checkbox}
/>
</FormItem>
</>
)}
</Card>
</div>
<Button block size="sm" variant="solid" loading={isSubmitting}>
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
{isSubmitting
? translate('::App.Platform.Saving')
: translate('::App.Platform.Save')}
</Button>
</FormContainer>
</Form>
)}
)
}}
</Formik>
</Container>
)

View file

@ -513,6 +513,7 @@ export function FormTabWorkflow(
dragPreview={dragPreview}
pendingLink={pendingLink}
selectedCriteriaId={selectedId}
dataSourceCode={listValues.dataSourceCode ?? ''}
userList={props.userList}
selectCommandColumns={props.selectCommandColumns}
isLoadingSelectCommandColumns={props.isLoadingSelectCommandColumns}

View file

@ -72,6 +72,7 @@ const initialValues: ListFormWizardDto = {
allowDeleting: true,
confirmDelete: true,
allowDetail: false,
openDetailAfterInsert: false,
seedData: false,
seedSync: false,
defaultLayout: 'grid',
@ -717,6 +718,7 @@ const Wizard = () => {
allowDeleting: w.allowDeleting ?? true,
confirmDelete: w.confirmDelete ?? true,
allowDetail: w.allowDetail ?? false,
openDetailAfterInsert: w.openDetailAfterInsert ?? false,
seedData: w.seedData ?? false,
seedSync: w.seedSync ?? false,
defaultLayout: w.defaultLayout ?? 'grid',
@ -1455,6 +1457,7 @@ const Wizard = () => {
{resolvedStep === STEP.workflow && (
<WizardStepWorkflow
listFormCode={values.listFormCode}
dataSourceCode={values.dataSourceCode}
workflow={workflow}
criteria={criteria}
selectCommandColumns={selectCommandColumns}

View file

@ -502,6 +502,19 @@ const WizardStepDataSettings = ({
>
<Field type="checkbox" autoComplete="off" name="allowDetail" component={Checkbox} />
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingOpenDetailAfterInsert')}
invalid={!!(errors.openDetailAfterInsert && touched.openDetailAfterInsert)}
errorMessage={errors.openDetailAfterInsert}
>
<Field
type="checkbox"
autoComplete="off"
name="openDetailAfterInsert"
component={Checkbox}
/>
</FormItem>
</div>
</div>

View file

@ -22,6 +22,7 @@ import { IdentityUserDto } from '@/proxy/admin/models'
type Props = {
listFormCode: string
dataSourceCode: string
workflow: WorkflowDto
criteria: ListFormWorkflowCriteriaDto[]
selectCommandColumns: DatabaseColumnDto[]
@ -106,6 +107,7 @@ const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteri
*/
function WizardStepWorkflow({
listFormCode,
dataSourceCode,
workflow,
criteria,
selectCommandColumns,
@ -521,6 +523,7 @@ function WizardStepWorkflow({
dragPreview={dragPreview}
pendingLink={pendingLink}
selectedCriteriaId={selectedId}
dataSourceCode={dataSourceCode}
userList={userList}
selectCommandColumns={selectCommandColumns}
isLoadingSelectCommandColumns={false}

View file

@ -1,7 +1,16 @@
import React, { useMemo } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { FaEdit, FaTrash } from 'react-icons/fa'
import classNames from 'classnames'
import { Button, Dialog, FormContainer, FormItem, Input, Select, Table } from '@/components/ui'
import {
Button,
Checkbox,
Dialog,
FormContainer,
FormItem,
Input,
Select,
Table,
} from '@/components/ui'
import TBody from '@/components/ui/Table/TBody'
import THead from '@/components/ui/Table/THead'
import Td from '@/components/ui/Table/Td'
@ -15,19 +24,26 @@ import {
splitWorkflowApprovers,
targetTitle,
} from '@/utils/workflow/workflowHelpers'
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
import type {
CompareOutcomeDto,
WorkflowCriteriaDto,
WorkflowRejectReasonDto,
} from '@/services/workflow.service'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { DatabaseColumnDto, DatabaseTableDto } from '@/proxy/sql-query-manager/models'
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
type TranslateFn = (key: string, params?: Record<string, string | number>) => string
type WorkflowCriteriaProps = {
criteria: WorkflowCriteriaDto[]
selectedId: string
formValues: any
busy: boolean
dataSourceCode: string
userList: SelectBoxOption[]
selectCommandColumns: DatabaseColumnDto[]
isLoadingSelectCommandColumns: boolean
@ -42,6 +58,7 @@ export function WorkflowCriteria({
selectedId,
formValues,
busy,
dataSourceCode,
userList,
selectCommandColumns,
isLoadingSelectCommandColumns,
@ -331,6 +348,15 @@ export function WorkflowCriteria({
)}
</div>
{formValues.kind === 'Approval' && (
<RejectReasonFields
value={formValues.rejectReason}
dataSourceCode={dataSourceCode}
recordColumns={compareColumnOptions}
onChange={(value) => setField('rejectReason', value)}
/>
)}
{formValues.kind === 'Compare' && (
<div className="mb-4">
<div className="mb-3 flex items-center justify-between gap-2">
@ -548,6 +574,126 @@ function SelectField({
)
}
const emptyRejectReason: WorkflowRejectReasonDto = {
tableName: '',
valueField: '',
textField: '',
targetField: '',
required: true,
}
/**
* Reddin listeden seçilen nedeni: tablo ve Id/metin kolonları ekranın veri kaynağından seçilir.
* Tablo boşsa karar diyaloğu yalnızca açıklama sorar.
*/
function RejectReasonFields({
value,
dataSourceCode,
recordColumns,
onChange,
}: {
value?: WorkflowRejectReasonDto | null
dataSourceCode: string
recordColumns: SelectBoxOption[]
onChange: (value: WorkflowRejectReasonDto | null) => void
}) {
const { translate } = useLocalization()
const reason = value ?? emptyRejectReason
const [tables, setTables] = useState<DatabaseTableDto[]>([])
const [columns, setColumns] = useState<DatabaseColumnDto[]>([])
useEffect(() => {
if (!dataSourceCode) return
sqlObjectManagerService
.getAllObjects(dataSourceCode)
.then((res) => setTables(res.data?.tables ?? []))
.catch(() => setTables([]))
}, [dataSourceCode])
const selectedTable = tables.find((table) => table.tableName === reason.tableName)
useEffect(() => {
if (!dataSourceCode || !selectedTable) {
setColumns([])
return
}
sqlObjectManagerService
.getTableColumns(dataSourceCode, selectedTable.schemaName, selectedTable.tableName)
.then((res) => setColumns(res.data ?? []))
.catch(() => setColumns([]))
}, [dataSourceCode, selectedTable])
const tableOptions = tables.map((table) => ({ value: table.tableName, label: table.tableName }))
const columnOptions = columns.map((column) => ({
value: column.columnName,
label: `${column.columnName} (${column.dataType})`,
}))
const update = (patch: Partial<WorkflowRejectReasonDto>) => onChange({ ...reason, ...patch })
const hasTable = Boolean(reason.tableName)
return (
<div className="mb-4 rounded border border-gray-200 p-3 dark:border-gray-700">
<h6 className="mb-1">{translate('::App.ListFormWorkflow.RejectReasonSection')}</h6>
<p className="mb-3 text-xs text-gray-500 dark:text-gray-400">
{translate('::App.ListFormWorkflow.RejectReasonHint')}
</p>
<div className="grid grid-cols-2 gap-1">
<FormItem label={translate('::App.ListFormWorkflow.RejectReasonTable')}>
<Select
isClearable
options={tableOptions}
value={tableOptions.find((option) => option.value === reason.tableName) ?? null}
onChange={(option: any) =>
option?.value
? update({ tableName: option.value, valueField: '', textField: '' })
: onChange(null)
}
/>
</FormItem>
<FormItem label={translate('::App.ListFormWorkflow.RejectReasonTargetField')}>
<Select
isClearable
isDisabled={!hasTable}
options={recordColumns}
value={recordColumns.find((option) => option.value === reason.targetField) ?? null}
onChange={(option: any) => update({ targetField: option?.value ?? '' })}
/>
</FormItem>
<FormItem
asterisk={hasTable}
label={translate('::App.ListFormWorkflow.RejectReasonValueField')}
>
<SelectField
required={hasTable}
options={columnOptions}
value={reason.valueField}
onChange={(valueField) => update({ valueField })}
/>
</FormItem>
<FormItem
asterisk={hasTable}
label={translate('::App.ListFormWorkflow.RejectReasonTextField')}
>
<SelectField
required={hasTable}
options={columnOptions}
value={reason.textField}
onChange={(textField) => update({ textField })}
/>
</FormItem>
<FormItem>
<Checkbox
disabled={!hasTable}
checked={reason.required !== false}
onChange={(required: boolean) => update({ required })}
>
{translate('::App.ListFormWorkflow.RejectReasonIsRequired')}
</Checkbox>
</FormItem>
</div>
</div>
)
}
function MultiSelectField({
options,
value,

View file

@ -21,6 +21,7 @@ type WorkflowDesignerProps = {
dragPreview: any
pendingLink: any
selectedCriteriaId: string
dataSourceCode: string
userList: SelectBoxOption[]
selectCommandColumns: DatabaseColumnDto[]
isLoadingSelectCommandColumns: boolean
@ -79,6 +80,7 @@ export function WorkflowDesigner({
dragPreview,
pendingLink,
selectedCriteriaId,
dataSourceCode,
userList,
selectCommandColumns,
isLoadingSelectCommandColumns,
@ -153,6 +155,7 @@ export function WorkflowDesigner({
selectedId={selectedCriteriaId}
formValues={criteriaForm}
busy={busy}
dataSourceCode={dataSourceCode}
userList={userList}
selectCommandColumns={selectCommandColumns}
isLoadingSelectCommandColumns={isLoadingSelectCommandColumns}

View file

@ -267,7 +267,7 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
},
{
id: '__UserId',
columnName: 'UserName',
columnName: 'ApprovalUserName',
dataType: 'nvarchar',
maxLength: '256',
isNullable: true,
@ -276,7 +276,7 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
},
{
id: '__Status',
columnName: 'Status',
columnName: 'ApprovalStatus',
dataType: 'nvarchar',
maxLength: '50',
isNullable: true,
@ -285,7 +285,7 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
},
{
id: '__Date',
columnName: 'Date',
columnName: 'ApprovalDate',
dataType: 'datetime',
maxLength: '',
isNullable: true,
@ -294,7 +294,7 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
},
{
id: '__Description',
columnName: 'Description',
columnName: 'ApprovalDescription',
dataType: 'nvarchar',
maxLength: '200',
isNullable: true,

View file

@ -10,7 +10,7 @@ import { isWorkflowToolbarItemDisabled, useToolbar } from '../list/useToolbar'
import { orderListToolbarItems } from '../list/toolbarOrder'
import { PermissionResults, RowMode } from './types'
import { GridDto } from '@/proxy/form/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { getFormRecordUrl, ROUTES_ENUM } from '@/routes/route.constant'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes } from '../admin/listForm/edit/types'
@ -268,11 +268,7 @@ const FormButtons = (props: {
if (onActionEdit) {
onActionEdit()
} else {
navigate(
ROUTES_ENUM.protected.admin.formEdit
.replace(':listFormCode', encodeURIComponent(listFormCode))
.replace(':id', encodeURIComponent(id!)),
)
navigate(getFormRecordUrl(listFormCode, id, true))
}
},
!permissions.u,
@ -289,11 +285,7 @@ const FormButtons = (props: {
if (onActionView) {
onActionView()
} else {
navigate(
ROUTES_ENUM.protected.admin.formView
.replace(':listFormCode', encodeURIComponent(listFormCode))
.replace(':id', encodeURIComponent(id!)),
)
navigate(getFormRecordUrl(listFormCode, id))
}
},
!permissions.r,

View file

@ -11,7 +11,7 @@ import { GridColumnData } from '../list/GridColumnData'
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
import { GridDto } from '@/proxy/form/models'
import { getAccessDeniedPath } from '@/utils/routing'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { getFormRecordUrl } from '@/routes/route.constant'
import { useLookupDataSource } from './useLookupDataSource'
import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
@ -154,18 +154,10 @@ const useGridData = (props: {
if (newId === undefined || newId === null) {
throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi'))
}
finishSubmit(
ROUTES_ENUM.protected.admin.formView
.replace(':listFormCode', encodeURIComponent(listFormCode))
.replace(':id', encodeURIComponent(String(newId))),
)
finishSubmit(getFormRecordUrl(listFormCode, newId))
} else if (mode === 'edit') {
await dataSource.update(id, buildUpdateData(formValues))
finishSubmit(
ROUTES_ENUM.protected.admin.formView
.replace(':listFormCode', encodeURIComponent(listFormCode))
.replace(':id', encodeURIComponent(id!)),
)
finishSubmit(getFormRecordUrl(listFormCode, id))
}
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {

View file

@ -68,7 +68,12 @@ import {
} from './shared/editingForm'
import { createEditingFormHandlers } from './shared/editingFormScripts'
import { flattenColumns } from './shared/columns'
import { getInitialExtraFilters, mergeFilterTriplets, useListFormWidgets } from './shared/hooks'
import {
getInitialExtraFilters,
mergeFilterTriplets,
useListFormWidgets,
useOpenDetailAfterInsert,
} from './shared/hooks'
import { useFilters } from './useFilters'
import { useListFormColumns } from './useListFormColumns'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
@ -162,7 +167,8 @@ const toCardColumn = (
showInColumnChooser: column.showInColumnChooser,
sortIndex: column.sortIndex,
sortOrder: column.sortOrder,
validationRules: column.validationRules,
// CardView formu şablonlu editörlere de kural uygular; liste kolonundan düşürülen kural buradan gelir.
validationRules: column.colData?.validationRuleDto ?? column.validationRules,
visible: column.visible,
visibleIndex: column.visibleIndex,
}).filter(([, value]) => value !== undefined),
@ -383,6 +389,7 @@ const CardView = (props: CardViewProps) => {
const [formData, setFormData] = useState<Record<string, any>>()
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const openDetailAfterInsert = useOpenDetailAfterInsert(gridDto)
const formDataRef = useRef<Record<string, any> | undefined>(undefined)
useEffect(() => {
formDataRef.current = formData
@ -1215,6 +1222,7 @@ const CardView = (props: CardViewProps) => {
onCardInserted={(e) => {
onCardChanged()
void refreshCardLookups(e.component, e.data)
openDetailAfterInsert(e)
}}
onCardUpdated={(e) => {
onCardChanged()

View file

@ -80,9 +80,11 @@ import {
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useOpenDetailAfterInsert,
useWidgetGroupHeight,
} from './shared/hooks'
import { startWorkflowForInsertedRow } from './shared/workflowRows'
import { cancelOnTemplateEditorErrors } from './shared/templateEditorValidation'
interface GridProps {
listFormCode: string
@ -130,6 +132,7 @@ const Grid = (props: GridProps) => {
} | null>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const openDetailAfterInsert = useOpenDetailAfterInsert(gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
@ -298,17 +301,20 @@ const Grid = (props: GridProps) => {
const onRowInserting = useCallback(
(e: DataGridTypes.RowInsertingEvent<any, any>) => {
if (cancelOnTemplateEditorErrors(e, gridDto, e.data, 'new', translate)) return
e.data = buildInsertPayload(gridDto, e.data)
},
[gridDto],
[gridDto, translate],
)
const onRowUpdating = useCallback(
(e: DataGridTypes.RowUpdatingEvent<any, any>) => {
if (!gridDto?.columnFormats) return
const data = { ...e.oldData, ...e.newData }
if (cancelOnTemplateEditorErrors(e, gridDto, data, 'edit', translate)) return
e.newData = buildUpdatePayload(gridDto, e.oldData, e.newData)
},
[gridDto],
[gridDto, translate],
)
const onEditingStart = useCallback(
@ -596,6 +602,7 @@ const Grid = (props: GridProps) => {
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
openDetailAfterInsert(e)
}}
onRowUpdated={(e) => {
props.refreshData?.()

View file

@ -50,6 +50,7 @@ import {
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useOpenDetailAfterInsert,
useWidgetGroupHeight,
} from './shared/hooks'
@ -79,6 +80,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
const layout = layoutTypes.scheduler || 'scheduler'
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const openDetailAfterInsert = useOpenDetailAfterInsert(gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
@ -832,7 +834,10 @@ const SchedulerView = (props: SchedulerViewProps) => {
onAppointmentDeleting={() => {
props.refreshData?.()
}}
onAppointmentAdded={() => void refreshWidgets()}
onAppointmentAdded={(e) => {
void refreshWidgets()
openDetailAfterInsert({ data: e.appointmentData })
}}
onAppointmentUpdated={() => void refreshWidgets()}
onAppointmentDeleted={() => void refreshWidgets()}
height={

View file

@ -76,9 +76,11 @@ import {
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useOpenDetailAfterInsert,
useWidgetGroupHeight,
} from './shared/hooks'
import { startWorkflowForInsertedRow } from './shared/workflowRows'
import { cancelOnTemplateEditorErrors } from './shared/templateEditorValidation'
interface TreeProps {
listFormCode: string
@ -121,6 +123,7 @@ const Tree = (props: TreeProps) => {
} | null>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const openDetailAfterInsert = useOpenDetailAfterInsert(gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
@ -309,17 +312,20 @@ const Tree = (props: TreeProps) => {
const onRowInserting = useCallback(
(e: TreeListTypes.RowInsertingEvent) => {
if (cancelOnTemplateEditorErrors(e, gridDto, e.data, 'new', translate)) return
e.data = buildInsertPayload(gridDto, e.data)
},
[gridDto],
[gridDto, translate],
)
const onRowUpdating = useCallback(
(e: TreeListTypes.RowUpdatingEvent) => {
if (!gridDto?.columnFormats) return
const data = { ...e.oldData, ...e.newData }
if (cancelOnTemplateEditorErrors(e, gridDto, data, 'edit', translate)) return
e.newData = buildUpdatePayload(gridDto, e.oldData, e.newData)
},
[gridDto],
[gridDto, translate],
)
const onEditingStart = useCallback(
@ -582,6 +588,7 @@ const Tree = (props: TreeProps) => {
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
openDetailAfterInsert(e)
}}
onRowUpdated={(e) => {
props.refreshData?.()

View file

@ -6,6 +6,7 @@
*/
import { captionize } from 'devextreme/core/utils/inflector'
import type { DataType } from 'devextreme/common'
import { formatMessage } from 'devextreme/localization'
import {
ColumnFormatDto,
DbTypeEnum,
@ -465,6 +466,55 @@ const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
return undefined
}
/**
* Grid/Tree'de `editCellTemplate` ile çizilen platform editörleri: lookup'lı TagBox/GridBox,
* lookup'sız ImageUpload/Rating. DevExtreme bu kolonlara `validationRules` uygulamaz (E1050).
*/
const hasCellEditTemplate = (colFormat: ColumnFormatDto) =>
colFormat.lookupDto?.dataSourceType
? colFormat.editorType2 === PlatformEditorTypes.dxTagBox ||
colFormat.editorType2 === PlatformEditorTypes.dxGridBox
: colFormat.editorType2 === PlatformEditorTypes.dxImageUpload ||
colFormat.editorType2 === PlatformEditorTypes.dxRating
const isEmptyValue = (value: unknown) =>
value === undefined ||
value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0)
/**
* Şablonlu editörlerin `required` kuralı DevExtreme'de çalışmadığı için kayıttan önce burada
* denetlenir; formda görünen boş zorunlu alanların hata metinleri döner.
*/
export const getTemplateEditorRequiredErrors = (
gridDto: GridDto | undefined,
data: Record<string, any>,
mode: Extract<RowMode, 'new' | 'edit'>,
translate: (key: string) => string,
) =>
(gridDto?.columnFormats ?? [])
.filter(
(colFormat) =>
colFormat.editOrderNo != null &&
hasCellEditTemplate(colFormat) &&
(mode === 'new'
? colFormat.canCreate && colFormat.allowAdding !== false
: colFormat.canUpdate && colFormat.allowEditing !== false),
)
.flatMap((colFormat) => {
const rule = colFormat.validationRuleDto?.find((item) => item.type === 'required')
if (!rule || !isEmptyValue(getValueByField(data, colFormat.fieldName))) return []
const caption = colFormat.captionName
? translate('::' + colFormat.captionName)
: colFormat.fieldName
return [
rule.message
? translate('::' + rule.message)
: formatMessage('validation-required-formatted', caption),
]
})
/** PlatformEditorTypes -> DevExtreme editorType eşlemesi. */
export const resolveEditorType = (editorType2?: EditorType2) => {
if (editorType2 === PlatformEditorTypes.dxGridBox) return 'dxDropDownBox'

View file

@ -3,8 +3,11 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { RefObject } from 'react'
import { useNavigate } from 'react-router-dom'
import type { GridDto } from '@/proxy/form/models'
import { getFormRecordUrl } from '@/routes/route.constant'
import { getList } from '@/services/form.service'
import { getPersistedInsertedKey } from './editingForm'
import {
addCss,
addJs,
@ -89,6 +92,28 @@ export const useListFormWidgets = (
return { widgets, refreshWidgets }
}
/**
* `EditingOptionDto.OpenDetailAfterInsert` açıksa popup'tan eklenen kaydın detay formuna geçer.
* Aynı sekmede açılır: kayıt sonrası asenkron `window.open` kullanıcı hareketi sayılmadığı için
* tarayıcının açılır pencere engeline takılırdı.
*/
export const useOpenDetailAfterInsert = (gridDto?: GridDto) => {
const navigate = useNavigate()
const enabled = !!gridDto?.gridOptions.editingOptionDto?.openDetailAfterInsert
const listFormCode = gridDto?.gridOptions.listFormCode
const keyFieldName = gridDto?.gridOptions.keyFieldName
return useCallback(
(event: any) => {
if (!enabled || !listFormCode) return
const key = getPersistedInsertedKey(event, keyFieldName)
if (key === undefined) return
navigate(getFormRecordUrl(listFormCode, key))
},
[enabled, listFormCode, keyFieldName, navigate],
)
}
/**
* WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin
* `calc(100vh - …)` hesabında kullanılır.

View file

@ -0,0 +1,29 @@
/**
* Grid ve Tree `onRowInserting` / `onRowUpdating` içinde şablonlu editörlerin zorunlu alan kontrolü.
*/
import { Notification, toast } from '@/components/ui'
import type { GridDto } from '@/proxy/form/models'
import { getTemplateEditorRequiredErrors } from './editingForm'
/** Boş zorunlu şablon alanı varsa kaydı iptal eder (popup açık kalır) ve hataları gösterir. */
export const cancelOnTemplateEditorErrors = (
event: { cancel?: unknown },
gridDto: GridDto | undefined,
data: Record<string, any>,
mode: 'new' | 'edit',
translate: (key: string) => string,
) => {
const errors = getTemplateEditorRequiredErrors(gridDto, data, mode, translate)
if (!errors.length) return false
event.cancel = true
toast.push(
<Notification type="danger" duration={4000}>
{errors.map((error) => (
<div key={error}>{error}</div>
))}
</Notification>,
{ placement: 'bottom-end' },
)
return true
}

View file

@ -6,6 +6,7 @@ import { useCallback, useEffect } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
import { getFormRecordUrl } from '@/routes/route.constant'
import { useDialogContext } from '../shared/DialogContext'
import { dynamicFetch } from '@/services/form.service'
import { GridColumnData } from './GridColumnData'
@ -454,10 +455,10 @@ const useListFormColumns = ({
if (!gridDto.gridOptions.keyFieldName) return
if (!gridDto.gridOptions.listFormCode) return
const detailFormName = gridDto.gridOptions.listFormCode
const id = e.row.data[gridDto.gridOptions.keyFieldName]
const url = `/admin/form/${encodeURIComponent(detailFormName)}/${encodeURIComponent(String(id))}`
const url = getFormRecordUrl(
gridDto.gridOptions.listFormCode,
e.row.data[gridDto.gridOptions.keyFieldName],
)
window.open(url, isPwaMode ? '_self' : '_blank')
},
@ -829,8 +830,9 @@ const useListFormColumns = ({
column.alignment = 'left'
}
if (colData.validationRuleDto) {
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
// Şablonlu editörde DevExtreme kuralı uygulamaz (E1050); zorunluluk kayıttan önce
// `cancelOnTemplateEditorErrors` ile denetlenir.
if (colData.validationRuleDto && !column.editCellTemplate) {
column.validationRules = colData.validationRuleDto as ValidationRule[]
}

View file

@ -1,4 +1,4 @@
import { Button, Notification, toast } from '@/components/ui'
import { Button, Notification, Radio, Select, toast } from '@/components/ui'
import { GridDto, UiCommandButtonPositionTypeEnum, WorkflowDto } from '@/proxy/form/models'
import { dynamicFetch } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -12,7 +12,11 @@ import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import type { WorkflowRunResultDto } from '@/services/workflow.service'
import type {
WorkflowRejectReasonDto,
WorkflowRejectReasonItemDto,
WorkflowRunResultDto,
} from '@/services/workflow.service'
import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
import { safeJsonParse } from './Utils'
import { BarcodeScannerDialog } from '@/components/shared'
@ -232,7 +236,8 @@ const useToolbar = ({
// baslangic degeri de mevcut secimden hesaplanir.
const currentSelection = resolveSelectedRowsData(getSelectedRowsData)
const workflowDisabled = (itemName: string) =>
isWorkflowToolbarItemDisabled(itemName, workflowOptions, currentSelection, currentUser) ?? true
isWorkflowToolbarItemDisabled(itemName, workflowOptions, currentSelection, currentUser) ??
true
if (
workflowOptions?.approvalStatusFieldName &&
approvalCriteria.length > 0 &&
@ -467,6 +472,7 @@ const useToolbar = ({
<>
<WorkflowApprovalDecisionDialog
criteriaTitle={criteria.title}
rejectReason={criteria.rejectReason}
keys={keys}
listFormCode={listFormCode}
criteriaId={criteria.id}
@ -572,9 +578,7 @@ const useToolbar = ({
open: true,
content: (
<>
<h5 className="mb-4">
{translate('::App.ListForm.DeleteSelectedRecords')}
</h5>
<h5 className="mb-4">{translate('::App.ListForm.DeleteSelectedRecords')}</h5>
<p>
{translate('::App.Platform.SeciliKayitlarSilmekIstiyormusunuz', {
@ -1040,9 +1044,7 @@ function WorkflowResetConfirmDialog({
return (
<>
<h5 className="mb-4">{translate('::App.ListForm.WorkflowReset')}</h5>
<p className="mb-4">
{translate('::App.ListForm.WorkflowResetMessage', { 0: keys.length })}
</p>
<p className="mb-4">{translate('::App.ListForm.WorkflowResetMessage', { 0: keys.length })}</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
@ -1062,6 +1064,7 @@ function WorkflowResetConfirmDialog({
function WorkflowApprovalDecisionDialog({
criteriaTitle,
rejectReason,
keys,
listFormCode,
criteriaId,
@ -1069,6 +1072,7 @@ function WorkflowApprovalDecisionDialog({
onCompleted,
}: {
criteriaTitle: string
rejectReason?: WorkflowRejectReasonDto | null
keys: unknown[]
listFormCode: string
criteriaId: string
@ -1076,15 +1080,40 @@ function WorkflowApprovalDecisionDialog({
onCompleted: () => void
}) {
const { translate } = useLocalization()
const [decision, setDecision] = useState<'approve' | 'reject' | ''>('')
const [note, setNote] = useState('')
const [submitting, setSubmitting] = useState(false)
const [reasons, setReasons] = useState<WorkflowRejectReasonItemDto[]>([])
const [reasonId, setReasonId] = useState('')
const hasReasonList = Boolean(rejectReason?.tableName)
const reasonRequired = hasReasonList && rejectReason?.required !== false
const showReasons = decision === 'reject' && hasReasonList
const canSave = decision !== '' && !(decision === 'reject' && reasonRequired && !reasonId)
const reasonOptions = reasons.map((reason) => ({ value: reason.id, label: reason.text }))
const decide = async (approved: boolean) => {
useEffect(() => {
if (!hasReasonList) return
workflowService
.getRejectReasons(listFormCode, criteriaId)
.then(setReasons)
.catch(() => setReasons([]))
}, [criteriaId, hasReasonList, listFormCode])
const save = async () => {
if (!canSave) return
const approved = decision === 'approve'
setSubmitting(true)
try {
const results = await Promise.all(
keys.map((key) =>
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
workflowService.decideWorkflow(
listFormCode,
[key],
approved,
note,
criteriaId,
approved ? undefined : reasonId || undefined,
),
),
)
showWorkflowToastMessages(results)
@ -1112,11 +1141,30 @@ function WorkflowApprovalDecisionDialog({
0: keys.length,
})}
</p>
<Radio.Group className="mb-4" value={decision} onChange={(value) => setDecision(value)}>
<Radio value="approve">{translate('::App.Listform.ListformField.Approver')}</Radio>
<Radio value="reject">{translate('::App.Listform.ListformField.Rejecter')}</Radio>
</Radio.Group>
{showReasons && (
<div className="mb-4">
<label className="mb-1 block text-sm font-semibold">
{translate('::App.WorkflowNote.RejectReason')}
{reasonRequired && <span className="ml-1 text-red-500">*</span>}
</label>
<Select
isClearable
menuPortalTarget={document.body}
placeholder={translate('::App.ListFormWorkflow.RejectReasonPlaceholder')}
options={reasonOptions}
value={reasonOptions.find((option) => option.value === reasonId) ?? null}
onChange={(option: any) => setReasonId(option?.value ?? '')}
/>
</div>
)}
<textarea
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
rows={4}
value={note}
autoFocus
placeholder={translate('::App.Listform.ListformField.ApprovalComment')}
onChange={(event) => setNote(event.target.value)}
/>
@ -1129,11 +1177,18 @@ function WorkflowApprovalDecisionDialog({
>
{translate('::App.Platform.Cancel')}
</Button>
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
{translate('::App.Listform.ListformField.Rejecter')}
</Button>
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
{translate('::App.Listform.ListformField.Approver')}
<Button
variant="solid"
loading={submitting}
disabled={!canSave}
title={
decision === 'reject' && reasonRequired && !reasonId
? translate('::App.ListFormWorkflow.RejectReasonRequired')
: undefined
}
onClick={save}
>
{translate('::App.Platform.Save')}
</Button>
</div>
</>