From c62e2c8586991d2832583b6b9fc400a8e2e953d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:45:44 +0300 Subject: [PATCH] IsLookupSelfSourced ve Widget tazelendi --- .../lowcode-reference.instructions.md | 4 + .github/instructions/lowcode.instructions.md | 10 +++ .../ListForms/Dto/ColumnFormatDto.cs | 8 ++ .../ListForms/ListFormSelectAppService.cs | 29 +++++++ .../2e84f333-5344-4f28-8cc0-178b46d7c0bc.json | 10 +-- ui/src/proxy/form/models.ts | 2 + ui/src/views/list/CardView.tsx | 39 +++++++-- ui/src/views/list/GanttView.tsx | 11 ++- ui/src/views/list/Grid.tsx | 26 ++++-- ui/src/views/list/SchedulerView.tsx | 20 +++-- ui/src/views/list/Tree.tsx | 26 ++++-- ui/src/views/list/shared/hooks.ts | 33 +++++++- ui/src/views/list/useListFormColumns.ts | 81 ++++++++++++++++++- .../views/list/useListFormCustomDataSource.ts | 4 +- ui/src/views/shared/lookupCache.ts | 7 ++ 15 files changed, 271 insertions(+), 39 deletions(-) diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index d58f8fcd..7a5d1a1a 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -428,6 +428,10 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, ` ve isteğe bağlı `Value` ifadesi; alt başlık ekranın adıdır, değer tip çakışmasın diye metne çevrilir. Gruplamalı sorgu için `DefaultWidgetsJson(sql, kartSayısı)`. Genel kart başlıkları `App.Widget.*` dil anahtarlarındadır. +- Kartlar GridDto ile gelir; Grid, Tree, Card, Gantt ve Scheduler görünümünde Yenile düğmesi ve + satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget + listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu + ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez. ### 5.4 `LayoutJson` (`LayoutDto`) diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index b2c57228..3c70d9eb 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -1207,6 +1207,16 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir. hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir sayı, `multiple` bir boolean olmalıdır. +Lookup kolonu hücre metnini açılışta yüklenen lookup listesinden bulur. Lookup'ı ekranın kendi +kayıt kaynağından beslenen kolonda (ör. `SELECT DISTINCT CompanyName … FROM Hr_T_Visitor` ile +Autocomplete) kaydedilen yeni değer bu listede yoktur. Sunucu bu kolonları ekran açılırken +işaretler (`ColumnFormatDto.IsLookupSelfSourced`: Query lookup'ının FROM/JOIN nesnesi ekranın +`SelectCommand` nesnesiyle aynı; yalnızca `true` yazılır, seed dosyasına girmez). Grid, Tree ve +Card görünümünde satır eklenince/güncellenince yalnızca işaretli kolonlar ve yalnızca kaydedilen +değer listede yoksa lookup yeniden yüklenir (`refreshLookupsForRow`) — hücre, editörün açılır +listesi ve filtre satırı sayfa yenilenmeden güncellenir. Başka kaynaktan beslenen lookup, statik +veri, WebService ve cascade lookup için kayıtta hiç lookup isteği atılmaz. + **Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` + `DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir: diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Dto/ColumnFormatDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Dto/ColumnFormatDto.cs index 17ec9873..a0064e24 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Dto/ColumnFormatDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Dto/ColumnFormatDto.cs @@ -184,6 +184,14 @@ public class ColumnFormatDto : AuditedEntityDto set { LookupJson = JsonSerializer.Serialize(value); } } + /// + /// Lookup sorgusu ekranın kendi kayıt kaynağını okuyor; kaydedilen yeni değer lookup listesine + /// ancak kayıttan sonra girdiği için istemci lookup'ı yalnızca bu alanlarda yeniler. Ekran + /// açılırken hesaplanır; false yazılmadığı için seed dosyasına girmez. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsLookupSelfSourced { get; set; } + [JsonIgnore] public string ValidationRuleJson { get; set; } public List ValidationRuleDto diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs index 80668856..5563436e 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; using Sozsoft.Platform.Queries; using Volo.Abp.Domain.Repositories; using Volo.Abp.Users; @@ -282,6 +283,33 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe return Widgets; } + /// + /// Query lookup'ı ekranın veri kaynağındaki bir nesneyi (FROM/JOIN) okuyan alanları işaretler. + /// WebService lookup'ının kaynağı sorgudan okunamadığı için dışarıda kalır. + /// + private static void MarkSelfSourcedLookups(ListForm listForm, List columnFormats) + { + var sourceObjects = listForm.SelectCommandType == SelectCommandTypeEnum.Query + ? WizardExportCollector.ExtractQueryObjectNames(listForm.SelectCommand).ToHashSet(StringComparer.OrdinalIgnoreCase) + : new HashSet(StringComparer.OrdinalIgnoreCase); + + if (listForm.SelectCommandType != SelectCommandTypeEnum.Query + && WizardExportCollector.NormalizeDatabaseObjectName(listForm.SelectCommand) is { } objectName) + { + sourceObjects.Add(objectName); + } + + if (sourceObjects.Count == 0) + return; + + foreach (var columnFormat in columnFormats.Where(c => !string.IsNullOrWhiteSpace(c.LookupJson))) + { + var lookup = columnFormat.LookupDto; + columnFormat.IsLookupSelfSourced = lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query + && WizardExportCollector.ExtractQueryObjectNames(lookup.LookupQuery).Any(sourceObjects.Contains); + } + } + public async Task GetGridAsync(string ListFormCode) { //Izin logic process @@ -306,6 +334,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe ColumnFormats = ObjectMapper.Map, List>(fields), Widgets = await GetWidgetsAsync(listForm) }; + MarkSelfSourcedLookups(listForm, result.ColumnFormats); //Workflow kriterlerini alıp GridOptionsDto içine atıyoruz ki frontend'de kullanabilelim var workflowDto = result.GridOptions.WorkflowDto; diff --git a/configs/seeds/host/data/App.DevRequests/2e84f333-5344-4f28-8cc0-178b46d7c0bc.json b/configs/seeds/host/data/App.DevRequests/2e84f333-5344-4f28-8cc0-178b46d7c0bc.json index 18abb5cd..5fa3954d 100644 --- a/configs/seeds/host/data/App.DevRequests/2e84f333-5344-4f28-8cc0-178b46d7c0bc.json +++ b/configs/seeds/host/data/App.DevRequests/2e84f333-5344-4f28-8cc0-178b46d7c0bc.json @@ -9,18 +9,18 @@ "TenantId": null, "UserName": "system@sozsoft.com", "RequestDate": null, - "Status": "Testing", + "Status": "Staging", "Title": "Human Resources -> Ziyaretçi Takip Listesi", "Description": "

Ziyaret Edilen Personel -> Kullanıcı Listesi üzerinden seçilecektir. Firma Adı AutoComplete yapıp ziyaretçi formuna girilen Firmalar listede gelecek istersek yeni Firma Adı girilebilmeli.



", "DueDate": "2026-09-30T11:47:00", "Tags": "HR", "Assignees": "system@sozsoft.com", "Priority": "5", - "Completed": false, - "SortOrder": 0, - "Timesheets": "[{\"id\":\"ca84a1de-f70f-6201-b713-3a240456d0d2\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0333},{\"id\":\"3c9ac80c-b091-750e-105b-3a240457f50b\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_Visitor tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"97079c2f-483a-c51d-9a63-3a24045a350c\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Visitor.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0333},{\"id\":\"dee98df8-0fbe-0829-93b7-3a24045a441c\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"2 tamamlanm\\u0131\\u015F alt g\\u00F6rev eklendi\",\"duration\":0.0167},{\"id\":\"8171cab5-bf91-b15f-07fd-3a24045a864f\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Analize \\u00FCretim notu eklendi\",\"duration\":0.0167}]", + "Completed": true, + "SortOrder": 1, + "Timesheets": "[{\"id\":\"ca84a1de-f70f-6201-b713-3a240456d0d2\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0333},{\"id\":\"3c9ac80c-b091-750e-105b-3a240457f50b\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Hr_T_Visitor tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"97079c2f-483a-c51d-9a63-3a24045a350c\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Visitor.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0333},{\"id\":\"dee98df8-0fbe-0829-93b7-3a24045a441c\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"2 tamamlanm\\u0131\\u015F alt g\\u00F6rev eklendi\",\"duration\":0.0167},{\"id\":\"8171cab5-bf91-b15f-07fd-3a24045a864f\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"Analize \\u00FCretim notu eklendi\",\"duration\":0.0167},{\"id\":\"c1e16604-5173-1655-c7bb-3a24045aab50\",\"date\":\"2026-09-30\",\"userName\":\"system@sozsoft.com\",\"description\":\"App.DevRequests ekran\\u0131nda sat\\u0131r g\\u00FCncellendi (Status)\",\"duration\":0.0167}]", "SubTasks": "[{\"id\":\"9763da5d-6dc7-a426-37e8-3a24045a440b\",\"title\":\"create_table: Hr_T_Visitor\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true},{\"id\":\"6f8de30c-593a-e9e6-6336-3a24045a440b\",\"title\":\"deploy_wizard: Visitor.json\",\"assignee\":\"system@sozsoft.com\",\"dueDate\":\"2026-09-30\",\"completed\":true}]", - "Analysis": "

1. Amaç

\n

Tesise gelen ziyaretçilerin giriş/çıkış kaydının İK ve güvenlik/resepsiyon tarafından tutulması; kimin, hangi firmadan, kimi, ne zaman ve neden ziyaret ettiğinin şube bazında izlenmesi ve o an içeride kimin bulunduğunun görülmesi.

\n\n

2. Görsel analizi

\n

[image-1] — Liste ekranı + popup düzenleme formu (klasik ListForm).

\n
    \n
  • Grid kolonları (sırasıyla): Ziyaret Tarihi (tarih), Ziyaretçi Tipi (seçim), Giriş Saati (saat), Çıkış Saati (saat), Kart No (metin), Ziyaret Edilen Personel (kullanıcı lookup), Ziyaretçi Adı Soyadı (metin), Firması (metin, öneri listeli), Ziyaret Sebebi (uzun metin), Şube Adı (şube lookup). Her kolonda filtre satırı açık.
  • \n
  • Form \"Ziyaretçi Takip Formu\" (tek grup, tek kolon): Şube Adı (seçim, \"1. Tesis\"), Ziyaretçi Tipi (açılır liste, varsayılan \"Ziyaretçi\"), Ziyaret Tarihi (varsayılan bugün), Giriş Saati (varsayılan şimdi, HH:mm:ss), Çıkış Saati (boş), Kart No, Ziyaretçi Adı Soyadı (geniş), Ziyaret Edilen Personel (açılır liste), Firma Adı (açılır liste), Ziyaret Sebebi (çok satırlı).
  • \n
  • Butonlar: Yeni / Kaydet / Sil / Kapat → platformun standart ekle-kaydet-sil-kapat davranışı; toolbar'daki Excel, filtre, arama, sıralama, grafik → standart grid araçları. Ek aksiyon yok.
  • \n
  • Görünür satır yok → demo veri üretilmez.
  • \n
\n\n

3. Çözüm yolu

\n

Wizard List (ComponentKind 0) — tek tablolu CRUD, lookup'lar ve varsayılan değerlerle karşılanır; özel bileşen, SP ya da servis gerekmez.

\n\n

4. Menü yeri

\n

İnsan Kaynakları › Ziyaretçi Takip Listesi

\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.Hrİnsan Kaynakları / Human ResourcesFcConferenceCallVar
EkranApp.Hr.VisitorZiyaretçiler / VisitorsFcBusinessmanYeni
\n

ModuleId: Hr · İzin grubu: App.Hr (Var) · Route: /admin/list/App.Hr.Visitor · Wizard: MenuParentCode = App.Hr, MenuParentPath boş (iki seviye).

\n

Yetki: App.Hr › App.Hr.Visitor — ara seviye yok; App.Hr.Visitor Yeni.

\n\n

5. Veri modeli

\n

Tablo: Hr_T_Visitor — Yeni, tenant kapsamlı (IsTenant = true, IsBranch = false; şube sütunu veridir), audit + soft delete alanları (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime).

\n

Tanım tablosu: Hayır — kayıtlar hareket/olay kaydıdır (her ziyaret bir satır), başka ekranın lookup kaynağı değildir; IsActive kolonu yoktur.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
KolonTipZorunluAçıklama / Lookup
Iduniqueidentifier (PK)Evet@NEWID
TenantIduniqueidentifierHayırPlatform doldurur
BranchIduniqueidentifierEvetŞube — LookupQueryValues.TenantBranchValues (Sas_T_Branch)
VisitorTypenvarchar(50)EvetStaticData: Ziyaretçi, Müşteri, Tedarikçi, Taşeron, Kurye
VisitDatedateEvetVarsayılan @NOW
EntryTimedatetimeEvetGiriş saati, varsayılan @NOW
ExitTimedatetimeHayırÇıkış saati; boşsa ziyaretçi içeride
CardNonvarchar(20)HayırVerilen ziyaretçi kartı
VisitorFullNamenvarchar(200)EvetZiyaretçi adı soyadı
VisitedUserIduniqueidentifierEvetKullanıcı listesi — SELECT Id AS [Key], Name + ' ' + Surname AS [Name] FROM AbpUsers WHERE IsDeleted = 'false' AND IsActive = 'true' AND TenantId = '@TENANTID' ORDER BY Name
CompanyNamenvarchar(200)HayırSerbest metin; öneriler aynı tablodaki önceki kayıtlardan — SELECT DISTINCT CompanyName AS [Key], CompanyName AS [Name] FROM Hr_T_Visitor WHERE IsDeleted = 'false' AND CompanyName IS NOT NULL AND TenantId = '@TENANTID' ORDER BY CompanyName
VisitReasonnvarchar(MAX)HayırÇok satırlı ziyaret sebebi
\n\n

6. Etkilenen kısımlar

\n
    \n
  • Yeni ekran App.Hr.Visitor + menü kaydı (İK kökü altında).
  • \n
  • Yetkiler: App.Hr.Visitor (R), .Create, .Update, .Delete, .Export, .Import, .Note.
  • \n
  • Dil anahtarları: App.Hr.Visitor, .Title, .Desc ve yeni alan başlıkları (VisitorType, VisitDate, EntryTime, ExitTime, CardNo, VisitorFullName, VisitedUserId, CompanyName, VisitReason; BranchId var olan anahtarı kullanır).
  • \n
  • Widget kartları (madde 7). Var olan ekranlara, iş akışına ya da bildirime etki yok; public yol yok (SEO yok).
  • \n
\n\n

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

\n
    \n
  • Layout: Grid (varsayılan), Card, Pivot, Chart açık. Varsayılan sıralama: VisitDate azalan, EntryTime azalan. Düzenleme modu popup (başlık \"Ziyaretçi Takip Formu\"), tek grup, ColCount 1.
  • \n
  • Form sırası: BranchId → VisitorType → VisitDate → EntryTime → ExitTime → CardNo → VisitorFullName → VisitedUserId → CompanyName → VisitReason.
  • \n
  • Grid sırası: VisitDate, VisitorType, EntryTime, ExitTime, CardNo, VisitedUserId, VisitorFullName, CompanyName, VisitReason, BranchId.
  • \n
  • Editörler: BranchId, VisitorType, VisitedUserId → dxSelectBox (searchEnabled: true); VisitDate → dxDateBox type date, dd.MM.yyyy; EntryTime / ExitTime → dxDateBox type time, HH:mm:ss, useMaskBehavior; CardNo, VisitorFullName → dxTextBox (maxLength 20 / 200); CompanyName → dxSelectBox acceptCustomValue: true, searchEnabled: true, showDataBeforeSearch: true (Key = Name = firma adı, metin olarak saklanır); VisitReason → dxTextArea, ColSpan tam.
  • \n
  • Varsayılanlar (insert): VisitorType = \"Ziyaretçi\", VisitDate = @NOW, EntryTime = @NOW; audit alanları @NOW/@USERID, IsDeleted = false, Id = @NEWID. Silme soft delete (UPDATE … IsDeleted = 'true').
  • \n
  • Doğrulama: BranchId, VisitorType, VisitDate, EntryTime, VisitorFullName, VisitedUserId zorunlu.
  • \n
  • Widget'lar (3 kart): \"Bugünkü Ziyaretçi\" (VisitDate = bugün, mavi), \"İçerideki Ziyaretçi\" (ExitTime IS NULL ve VisitDate = bugün, turuncu), \"Çıkış Yapan\" (bugün ExitTime dolu, yeşil); tümü TenantId ve IsDeleted = 'false' süzgeçli.
  • \n
\n\n

8. Kararlar

\n
    \n
  • Ekran kodu App.Hr.Visitor, tablo Hr_T_Visitor — İK önek kuralı (reference §12.4).
  • \n
  • Firma Adı \"AutoComplete\" isteği dxSelectBox + acceptCustomValue ile karşılanır: önceki kayıtlardaki firmalar listelenir, listede olmayan yeni ad yazılabilir; ayrı firma tanım tablosu açılmaz, çünkü talep önerilerin ziyaretçi formuna girilenlerden gelmesini istiyor.
  • \n
  • Ziyaretçi Tipi değerleri: Ziyaretçi, Müşteri, Tedarikçi, Taşeron, Kurye — görselde yalnız \"Ziyaretci\" görünüyor, liste analizde tamamlandı (onay).
  • \n
  • Giriş/Çıkış saati datetime kolonda saat editörüyle tutulur; ayrı time tipi kullanılmaz (tarih editörü sözleşmesi Date/DateTime tiplerini destekler).
  • \n
  • Şube Adı formda zorunlu ve seçilebilir alan olarak üretilir; ekran IsBranch = false kalır, yani satırlar kullanıcının şube atamasına göre süzülmez (onay).
  • \n
  • Ziyaret Edilen Personel yalnız aktif ve silinmemiş kullanıcılardan seçilir; kayıtta kullanıcı Id'si saklanır, grid'de ad soyad gösterilir.
  • \n
  • Çıkış saati boş kayıt \"içeride\" sayılır; çıkış saati ≥ giriş saati kontrolü yapılmaz (gece yarısını aşan ziyaret senaryosu) (onay).
  • \n
  • Demo veri yok (görselde satır yok).
  • \n
\n\n

9. Üretim planı

\n
    \n
  1. create_table: Hr_T_Visitor
  2. \n
  3. deploy_wizard: Visitor.json
  4. \n
\n\n

10. Üretime hazır

\n

Evet

MCP

\n
    \n
  • Tablo: Hr_T_Visitor oluşturuldu (tenant + tam audit; iş kolonları analizdeki gibi, VisitorType DB varsayılanı 'Ziyaretçi'). Seed: host/sql/object/Hr_T_Visitor.sql.
  • \n
  • Ekran: App.Hr.Visitor (Visitor.json) — Grid/Card/Pivot/Chart, popup form \"Ziyaretçi Takip Formu\" (tek grup, 1 kolon), IsTenant = true, IsBranch = false, soft delete.
  • \n
  • Menü / yetki: İnsan Kaynakları › Ziyaretçi Takip Listesi (/admin/list/App.Hr.Visitor, FcBusinessman); yetkiler App.Hr.Visitor + .Create/.Update/.Delete/.Export/.Import/.Note, izin grubu App.Hr. Roller yetkiyi rol ekranından almalı.
  • \n
  • Lookup'lar: Şube (Sas_T_Branch, aktif), Ziyaret Edilen Personel (AbpUsers, aktif; ad + soyad), Ziyaretçi Tipi (StaticData 5 değer), Firma Adı (aynı tablodaki farklı firma adları; acceptCustomValue ile yeni ad yazılabilir).
  • \n
  • Widget'lar: Bugünkü Ziyaretçi, İçerideki Ziyaretçi, Çıkış Yapan (yeni dil anahtarları App.Hr.Visitor.Widget.Today/Inside/Exited).
  • \n
  • Varsayılanlar (uygulamada tamamlanan): Wizard'da form varsayılanı ve sıralama adımı olmadığı için Ziyaret Tarihi, Giriş Saati (şimdi) ve Ziyaretçi Tipi (\"Ziyaretçi\") form açılışında alan boşsa EditorScript (@runOnOpen) ile doldurulur; düzenlenen kayıtta değer korunur. Varsayılan sıralama (VisitDate azalan) ve popup başlığı wizard'da taşınamadı — gerekirse ListForm editöründen ayarlanır.
  • \n
  • Alan sırası: Wizard grid ve form sırasını tek listeden türettiği için görseldeki grid sırası kullanıldı (Ziyaret Tarihi … Şube Adı); formda da aynı sıra geçerli.
  • \n
  • Demo veri: yok.
  • \n
  • DB Migrate bekleyen wizard: Visitor.json.
  • \n
" + "Analysis": "

1. Amaç

\n

Tesise gelen ziyaretçilerin giriş/çıkış kaydının İK ve güvenlik/resepsiyon tarafından tutulması; kimin, hangi firmadan, kimi, ne zaman ve neden ziyaret ettiğinin şube bazında izlenmesi ve o an içeride kimin bulunduğunun görülmesi.

\n\n

2. Görsel analizi

\n

[image-1] — Liste ekranı + popup düzenleme formu (klasik ListForm).

\n
    \n
  • Grid kolonları (sırasıyla): Ziyaret Tarihi (tarih), Ziyaretçi Tipi (seçim), Giriş Saati (saat), Çıkış Saati (saat), Kart No (metin), Ziyaret Edilen Personel (kullanıcı lookup), Ziyaretçi Adı Soyadı (metin), Firması (metin, öneri listeli), Ziyaret Sebebi (uzun metin), Şube Adı (şube lookup). Her kolonda filtre satırı açık.
  • \n
  • Form \"Ziyaretçi Takip Formu\" (tek grup, tek kolon): Şube Adı (seçim, \"1. Tesis\"), Ziyaretçi Tipi (açılır liste, varsayılan \"Ziyaretçi\"), Ziyaret Tarihi (varsayılan bugün), Giriş Saati (varsayılan şimdi, HH:mm:ss), Çıkış Saati (boş), Kart No, Ziyaretçi Adı Soyadı (geniş), Ziyaret Edilen Personel (açılır liste), Firma Adı (açılır liste), Ziyaret Sebebi (çok satırlı).
  • \n
  • Butonlar: Yeni / Kaydet / Sil / Kapat → platformun standart ekle-kaydet-sil-kapat davranışı; toolbar'daki Excel, filtre, arama, sıralama, grafik → standart grid araçları. Ek aksiyon yok.
  • \n
  • Görünür satır yok → demo veri üretilmez.
  • \n
\n\n

3. Çözüm yolu

\n

Wizard List (ComponentKind 0) — tek tablolu CRUD, lookup'lar ve varsayılan değerlerle karşılanır; özel bileşen, SP ya da servis gerekmez.

\n\n

4. Menü yeri

\n

İnsan Kaynakları › Ziyaretçi Takip Listesi

\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.Hrİnsan Kaynakları / Human ResourcesFcConferenceCallVar
EkranApp.Hr.VisitorZiyaretçi Takip Listesi / Visitor Tracking ListFcBusinessmanYeni
\n

ModuleId: Hr · İzin grubu: App.Hr (Var) · Route: /admin/list/App.Hr.Visitor · Wizard: MenuParentCode = App.Hr, MenuParentPath boş (iki seviye).

\n

Yetki: App.Hr › App.Hr.Visitor — ara seviye yok; App.Hr.Visitor Yeni.

\n\n

5. Veri modeli

\n

Tablo: Hr_T_Visitor — Yeni, tenant kapsamlı (IsTenant = true, IsBranch = false; şube sütunu veridir), audit + soft delete alanları (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime).

\n

Tanım tablosu: Hayır — kayıtlar hareket/olay kaydıdır (her ziyaret bir satır), başka ekranın lookup kaynağı değildir; IsActive kolonu yoktur.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
KolonTipZorunluAçıklama / Lookup
Iduniqueidentifier (PK)Evet@NEWID
TenantIduniqueidentifierHayırPlatform doldurur
BranchIduniqueidentifierEvetŞube — LookupQueryValues.TenantBranchValues (Sas_T_Branch)
VisitorTypenvarchar(50)EvetStaticData: Ziyaretçi, Müşteri, Tedarikçi, Taşeron, Kurye
VisitDatedateEvetVarsayılan @NOW
EntryTimedatetimeEvetGiriş saati, varsayılan @NOW
ExitTimedatetimeHayırÇıkış saati; boşsa ziyaretçi içeride
CardNonvarchar(20)HayırVerilen ziyaretçi kartı
VisitorFullNamenvarchar(200)EvetZiyaretçi adı soyadı
VisitedUserIduniqueidentifierEvetKullanıcı listesi — SELECT Id AS [Key], Name + ' ' + Surname AS [Name] FROM AbpUsers WHERE IsDeleted = 'false' AND IsActive = 'true' AND TenantId = '@TENANTID' ORDER BY Name
CompanyNamenvarchar(200)HayırSerbest metin; öneriler aynı tablodaki önceki kayıtlardan — SELECT DISTINCT CompanyName AS [Key], CompanyName AS [Name] FROM Hr_T_Visitor WHERE IsDeleted = 'false' AND CompanyName IS NOT NULL AND TenantId = '@TENANTID' ORDER BY CompanyName
VisitReasonnvarchar(MAX)HayırÇok satırlı ziyaret sebebi
\n\n

6. Etkilenen kısımlar

\n
    \n
  • Yeni ekran App.Hr.Visitor + menü kaydı (İK kökü altında).
  • \n
  • Yetkiler: App.Hr.Visitor (R), .Create, .Update, .Delete, .Export, .Import, .Note.
  • \n
  • Dil anahtarları: App.Hr.Visitor, .Title, .Desc ve yeni alan başlıkları (VisitorType, VisitDate, EntryTime, ExitTime, CardNo, VisitorFullName, VisitedUserId, CompanyName, VisitReason; BranchId var olan anahtarı kullanır).
  • \n
  • Widget kartları (madde 7). Var olan ekranlara, iş akışına ya da bildirime etki yok; public yol yok (SEO yok).
  • \n
\n\n

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

\n
    \n
  • Layout: Grid (varsayılan), Card, Pivot, Chart açık. Varsayılan sıralama: VisitDate azalan, EntryTime azalan. Düzenleme modu popup (başlık \"Ziyaretçi Takip Formu\"), tek grup, ColCount 1.
  • \n
  • Form sırası: BranchId → VisitorType → VisitDate → EntryTime → ExitTime → CardNo → VisitorFullName → VisitedUserId → CompanyName → VisitReason.
  • \n
  • Grid sırası: VisitDate, VisitorType, EntryTime, ExitTime, CardNo, VisitedUserId, VisitorFullName, CompanyName, VisitReason, BranchId.
  • \n
  • Editörler: BranchId, VisitorType, VisitedUserId → dxSelectBox (searchEnabled: true); VisitDate → dxDateBox type date, dd.MM.yyyy; EntryTime / ExitTime → dxDateBox type time, HH:mm:ss, useMaskBehavior; CardNo, VisitorFullName → dxTextBox (maxLength 20 / 200); CompanyName → dxSelectBox acceptCustomValue: true, searchEnabled: true, showDataBeforeSearch: true (Key = Name = firma adı, metin olarak saklanır); VisitReason → dxTextArea, ColSpan tam.
  • \n
  • Varsayılanlar (insert): VisitorType = \"Ziyaretçi\", VisitDate = @NOW, EntryTime = @NOW; audit alanları @NOW/@USERID, IsDeleted = false, Id = @NEWID. Silme soft delete (UPDATE … IsDeleted = 'true').
  • \n
  • Doğrulama: BranchId, VisitorType, VisitDate, EntryTime, VisitorFullName, VisitedUserId zorunlu.
  • \n
  • Widget'lar (3 kart): \"Bugünkü Ziyaretçi\" (VisitDate = bugün, mavi), \"İçerideki Ziyaretçi\" (ExitTime IS NULL ve VisitDate = bugün, turuncu), \"Çıkış Yapan\" (bugün ExitTime dolu, yeşil); tümü TenantId ve IsDeleted = 'false' süzgeçli.
  • \n
\n\n

8. Kararlar

\n
    \n
  • Ekran kodu App.Hr.Visitor, tablo Hr_T_Visitor — İK önek kuralı (reference §12.4).
  • \n
  • Firma Adı \"AutoComplete\" isteği dxSelectBox + acceptCustomValue ile karşılanır: önceki kayıtlardaki firmalar listelenir, listede olmayan yeni ad yazılabilir; ayrı firma tanım tablosu açılmaz, çünkü talep önerilerin ziyaretçi formuna girilenlerden gelmesini istiyor.
  • \n
  • Ziyaretçi Tipi değerleri: Ziyaretçi, Müşteri, Tedarikçi, Taşeron, Kurye — görselde yalnız \"Ziyaretci\" görünüyor, liste analizde tamamlandı (onay).
  • \n
  • Giriş/Çıkış saati datetime kolonda saat editörüyle tutulur; ayrı time tipi kullanılmaz (tarih editörü sözleşmesi Date/DateTime tiplerini destekler).
  • \n
  • Şube Adı formda zorunlu ve seçilebilir alan olarak üretilir; ekran IsBranch = false kalır, yani satırlar kullanıcının şube atamasına göre süzülmez (onay).
  • \n
  • Ziyaret Edilen Personel yalnız aktif ve silinmemiş kullanıcılardan seçilir; kayıtta kullanıcı Id'si saklanır, grid'de ad soyad gösterilir.
  • \n
  • Çıkış saati boş kayıt \"içeride\" sayılır; çıkış saati ≥ giriş saati kontrolü yapılmaz (gece yarısını aşan ziyaret senaryosu) (onay).
  • \n
  • Demo veri yok (görselde satır yok).
  • \n
\n\n

9. Üretim planı

\n
    \n
  1. create_table: Hr_T_Visitor
  2. \n
  3. deploy_wizard: Visitor.json
  4. \n
\n\n

10. Üretime hazır

\n

Evet

MCP

\n
    \n
  • Tablo: Hr_T_Visitor oluşturuldu (tenant + tam audit; iş kolonları analizdeki gibi, VisitorType DB varsayılanı 'Ziyaretçi'). Seed: host/sql/object/Hr_T_Visitor.sql.
  • \n
  • Ekran: App.Hr.Visitor (Visitor.json) — Grid/Card/Pivot/Chart, popup form \"Ziyaretçi Takip Formu\" (tek grup, 1 kolon), IsTenant = true, IsBranch = false, soft delete.
  • \n
  • Menü / yetki: İnsan Kaynakları › Ziyaretçi Takip Listesi (/admin/list/App.Hr.Visitor, FcBusinessman); yetkiler App.Hr.Visitor + .Create/.Update/.Delete/.Export/.Import/.Note, izin grubu App.Hr. Roller yetkiyi rol ekranından almalı.
  • \n
  • Lookup'lar: Şube (Sas_T_Branch, aktif), Ziyaret Edilen Personel (AbpUsers, aktif; ad + soyad), Ziyaretçi Tipi (StaticData 5 değer), Firma Adı (aynı tablodaki farklı firma adları; acceptCustomValue ile yeni ad yazılabilir).
  • \n
  • Widget'lar: Bugünkü Ziyaretçi, İçerideki Ziyaretçi, Çıkış Yapan (yeni dil anahtarları App.Hr.Visitor.Widget.Today/Inside/Exited).
  • \n
  • Varsayılanlar (uygulamada tamamlanan): Wizard'da form varsayılanı ve sıralama adımı olmadığı için Ziyaret Tarihi, Giriş Saati (şimdi) ve Ziyaretçi Tipi (\"Ziyaretçi\") form açılışında alan boşsa EditorScript (@runOnOpen) ile doldurulur; düzenlenen kayıtta değer korunur. Varsayılan sıralama (VisitDate azalan) ve popup başlığı wizard'da taşınamadı — gerekirse ListForm editöründen ayarlanır.
  • \n
  • Alan sırası: Wizard grid ve form sırasını tek listeden türettiği için görseldeki grid sırası kullanıldı (Ziyaret Tarihi … Şube Adı); formda da aynı sıra geçerli.
  • \n
  • Demo veri: yok.
  • \n
  • DB Migrate bekleyen wizard: Visitor.json.
  • \n
" } ] } diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 678dc043..8585b4dd 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -227,6 +227,8 @@ export interface ColumnFormatDto extends AuditedEntityDto { columnGroupSummaryDto: ColumnTotalSummaryDto lookupJson?: string lookupDto: LookupDto + /** Lookup ekranın kendi kayıt kaynağından besleniyor; kayıttan sonra lookup yenilenir. */ + isLookupSelfSourced?: boolean validationRuleJson?: string validationRuleDto: ValidationRuleDto[] columnStylingJson?: string diff --git a/ui/src/views/list/CardView.tsx b/ui/src/views/list/CardView.tsx index 35d20df7..e894cf4e 100644 --- a/ui/src/views/list/CardView.tsx +++ b/ui/src/views/list/CardView.tsx @@ -60,7 +60,7 @@ import { import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm' import { createEditingFormHandlers } from './shared/editingFormScripts' import { flattenColumns } from './shared/columns' -import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks' +import { getInitialExtraFilters, mergeFilterTriplets, useListFormWidgets } from './shared/hooks' import { useFilters } from './useFilters' import { useListFormColumns } from './useListFormColumns' import { useListFormCustomDataSource } from './useListFormCustomDataSource' @@ -373,6 +373,7 @@ const CardView = (props: CardViewProps) => { const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [formData, setFormData] = useState>() const widgetGroupRef = useRef(null) + const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const formDataRef = useRef | undefined>(undefined) useEffect(() => { formDataRef.current = formData @@ -447,7 +448,12 @@ const CardView = (props: CardViewProps) => { ;(cardDataSource?.store() as any)?.invalidateSelectCache?.() const reload = cardDataSource?.reload() Promise.resolve(reload).finally(() => refreshParentData?.()) - }, [refreshParentData]) + void refreshWidgets() + }, [refreshParentData, refreshWidgets]) + const onCardChanged = useCallback(() => { + refreshParentData?.() + void refreshWidgets() + }, [refreshParentData, refreshWidgets]) const setCardColumnCount = useCallback( (cardColumnCount: number) => { setListState({ @@ -477,7 +483,7 @@ const CardView = (props: CardViewProps) => { [gridDto, listFormCode], ) - const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({ + const { getBandedColumns, loadLookupDisplayValues, refreshLookupsForRow } = useListFormColumns({ gridDto, listFormCode, isSubForm, @@ -729,6 +735,17 @@ const CardView = (props: CardViewProps) => { ]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues })) }, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues]) + // Kart metinleri lookup haritasından okunur; yenilenen lookup haritaya işlenip kartlar yeniden çizilir. + const refreshCardLookups = useCallback( + async (instance: any, rowData?: Record) => { + if (!(await refreshLookupsForRow(instance, rowData))) return + + applyLookupDisplayValues(await loadLookupDisplayValues(getLookupColumns())) + instance.repaint?.() + }, + [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues, refreshLookupsForRow], + ) + const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef }) const dataSource = useMemo | undefined>(() => { if (!gridDto) return undefined @@ -1102,8 +1119,8 @@ const CardView = (props: CardViewProps) => { return ( <> -
0 ? 'mt-2' : ''}> - +
0 ? 'mt-2' : ''}> +
@@ -1162,9 +1179,15 @@ const CardView = (props: CardViewProps) => { { placement: 'bottom-end' }, ) }} - onCardInserted={() => refreshParentData?.()} - onCardUpdated={() => refreshParentData?.()} - onCardRemoved={() => refreshParentData?.()} + onCardInserted={(e) => { + onCardChanged() + void refreshCardLookups(e.component, e.data) + }} + onCardUpdated={(e) => { + onCardChanged() + void refreshCardLookups(e.component, e.data) + }} + onCardRemoved={onCardChanged} > { const pendingStateRef = useRef | undefined>(undefined) const loadedStateKeyRef = useRef('') - const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets) + const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) + const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) useListFormCustomSources(gridDto) const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({ @@ -249,9 +251,10 @@ const GanttView = (props: GanttViewProps) => { stylingMode: 'icon', onClick: () => { gridRef.current?.instance()?.refresh() + void refreshWidgets() }, }), - [translate], + [translate, refreshWidgets], ) // Gantt'in kendi sutun secicisi yok; ici TreeList oldugu icin onun secicisi acilir. @@ -424,8 +427,8 @@ const GanttView = (props: GanttViewProps) => { return ( <> -
0 ? 'mt-2' : ''}> - +
0 ? 'mt-2' : ''}> +
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 403e19c3..d643728b 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -77,6 +77,7 @@ import { useExtraFilters, useListFormCustomSources, useListFormGridDto, + useListFormWidgets, useWidgetGroupHeight, } from './shared/hooks' import { startWorkflowForInsertedRow } from './shared/workflowRows' @@ -127,7 +128,8 @@ const Grid = (props: GridProps) => { entityId: string } | null>(null) - const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets) + const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) + const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) useListFormCustomSources(gridDto) const getGridInstance = useCallback(() => gridRef.current?.instance(), []) @@ -175,7 +177,7 @@ const Grid = (props: GridProps) => { [gridDto, listFormCode], ) - const { getBandedColumns } = useListFormColumns({ + const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({ gridDto, listFormCode, isSubForm, @@ -205,7 +207,8 @@ const Grid = (props: GridProps) => { const refreshData = useCallback(() => { const grid = getGridInstance() Promise.resolve(grid?.refresh()).finally(() => updateWorkflowApprovalButtons(grid)) - }, [getGridInstance, updateWorkflowApprovalButtons]) + void refreshWidgets() + }, [getGridInstance, updateWorkflowApprovalButtons, refreshWidgets]) const getSelectedRowKeys = useCallback( async () => (await getGridInstance()?.getSelectedRowKeys()) ?? [], @@ -559,8 +562,8 @@ const Grid = (props: GridProps) => { return ( <> -
0 ? 'mt-2' : ''}> - +
0 ? 'mt-2' : ''}> +
@@ -623,9 +626,18 @@ const Grid = (props: GridProps) => { onRowInserted={(e) => { startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshGrid) props.refreshData?.() + void refreshWidgets() + void refreshLookupsForRow(e.component, e.data) + }} + onRowUpdated={(e) => { + props.refreshData?.() + void refreshWidgets() + void refreshLookupsForRow(e.component, e.data) + }} + onRowRemoved={() => { + props.refreshData?.() + void refreshWidgets() }} - onRowUpdated={() => props.refreshData?.()} - onRowRemoved={() => props.refreshData?.()} onEditorPreparing={onEditorPreparing} > { const [userAvatarUrls, setUserAvatarUrls] = useState>({}) const layout = layoutTypes.scheduler || 'scheduler' - const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets) + const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) + const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) useListFormCustomSources(gridDto) useEffect(() => { @@ -161,7 +167,8 @@ const SchedulerView = (props: SchedulerViewProps) => { dataSource.reload() } } - }, []) + void refreshWidgets() + }, [refreshWidgets]) const onCurrentViewChange = useCallback((value: string) => { setCurrentView(value) @@ -769,8 +776,8 @@ const SchedulerView = (props: SchedulerViewProps) => { return ( <> -
0 ? 'mt-2' : ''}> - +
0 ? 'mt-2' : ''}> +
@@ -820,6 +827,9 @@ const SchedulerView = (props: SchedulerViewProps) => { onAppointmentDeleting={() => { props.refreshData?.() }} + onAppointmentAdded={() => void refreshWidgets()} + onAppointmentUpdated={() => void refreshWidgets()} + onAppointmentDeleted={() => void refreshWidgets()} height={ gridDto.gridOptions.height > 0 ? gridDto.gridOptions.height diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index 54bbd3a6..abca289a 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -73,6 +73,7 @@ import { useExtraFilters, useListFormCustomSources, useListFormGridDto, + useListFormWidgets, useWidgetGroupHeight, } from './shared/hooks' import { startWorkflowForInsertedRow } from './shared/workflowRows' @@ -117,7 +118,8 @@ const Tree = (props: TreeProps) => { entityId: string } | null>(null) - const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets) + const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) + const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) useListFormCustomSources(gridDto) const getTreeInstance = useCallback(() => gridRef.current?.instance(), []) @@ -165,7 +167,7 @@ const Tree = (props: TreeProps) => { [gridDto, listFormCode], ) - const { getBandedColumns } = useListFormColumns({ + const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({ gridDto, listFormCode, isSubForm, @@ -195,7 +197,8 @@ const Tree = (props: TreeProps) => { const refreshData = useCallback(() => { const tree = getTreeInstance() Promise.resolve(tree?.refresh()).finally(() => updateWorkflowApprovalButtons(tree)) - }, [getTreeInstance, updateWorkflowApprovalButtons]) + void refreshWidgets() + }, [getTreeInstance, updateWorkflowApprovalButtons, refreshWidgets]) const getSelectedRowKeys = useCallback( async () => (await getTreeInstance()?.getSelectedRowKeys()) ?? [], @@ -535,8 +538,8 @@ const Tree = (props: TreeProps) => { return ( <> -
0 ? 'mt-2' : ''}> - +
0 ? 'mt-2' : ''}> +
@@ -605,9 +608,18 @@ const Tree = (props: TreeProps) => { onRowInserted={(e) => { startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshTree) props.refreshData?.() + void refreshWidgets() + void refreshLookupsForRow(e.component, e.data) + }} + onRowUpdated={(e) => { + props.refreshData?.() + void refreshWidgets() + void refreshLookupsForRow(e.component, e.data) + }} + onRowRemoved={() => { + props.refreshData?.() + void refreshWidgets() }} - onRowUpdated={() => props.refreshData?.()} - onRowRemoved={() => props.refreshData?.()} onEditorPreparing={onEditorPreparing} onContentReady={(e) => { updateWorkflowApprovalButtons(e.component) diff --git a/ui/src/views/list/shared/hooks.ts b/ui/src/views/list/shared/hooks.ts index 17647159..98238fc5 100644 --- a/ui/src/views/list/shared/hooks.ts +++ b/ui/src/views/list/shared/hooks.ts @@ -1,7 +1,7 @@ /** * Liste görünümlerinin paylaştığı küçük React hook'ları. */ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { RefObject } from 'react' import type { GridDto } from '@/proxy/form/models' import { getList } from '@/services/form.service' @@ -48,6 +48,37 @@ export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridD return gridDto } +/** + * Widget değerleri GridDto ile bir kez hesaplanır; görünüm yenilendiğinde ya da satır + * eklendiğinde/değiştiğinde değerler bayatlamasın diye `refreshWidgets` yalnızca + * widget'ları yeniden çeker. GridDto'nun kendisi değiştirilmez: yeni referans kolonları + * ve veri kaynağını baştan kurar, görünüm durumunu (sayfa, seçim, açık satır) sıfırlardı. + */ +export const useListFormWidgets = (listFormCode: string, gridDto?: GridDto) => { + const [state, setState] = useState<{ source?: GridDto; widgets: GridDto['widgets'] }>({ + widgets: [], + }) + const requestIdRef = useRef(0) + + const widgets = state.source === gridDto ? state.widgets : (gridDto?.widgets ?? []) + const hasWidgets = (gridDto?.widgets?.length ?? 0) > 0 + + const refreshWidgets = useCallback(async () => { + if (!listFormCode || !gridDto || !hasWidgets) return + + const requestId = ++requestIdRef.current + try { + const response = await getList({ listFormCode }) + if (requestId !== requestIdRef.current) return + setState({ source: gridDto, widgets: response.data?.widgets ?? [] }) + } catch (error) { + console.error('Widget refresh error:', error) + } + }, [listFormCode, gridDto, hasWidgets]) + + return { widgets, refreshWidgets } +} + /** * WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin * `calc(100vh - …)` hesabında kullanılır. diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts index f915a18e..81334634 100644 --- a/ui/src/views/list/useListFormColumns.ts +++ b/ui/src/views/list/useListFormColumns.ts @@ -17,7 +17,11 @@ import { UiLookupDataSourceTypeEnum, } from '@/proxy/form/models' import { addCss, getEditingFormFields } from './Utils' -import { cachedLookupLoad, clearLookupCache } from '../shared/lookupCache' +import { + cachedLookupLoad, + clearLookupCache, + clearLookupCacheByPrefix, +} from '../shared/lookupCache' import { NO_IMAGE, normalizeImageSize, @@ -269,6 +273,80 @@ const useListFormColumns = ({ [listFormCode, lookupDataSource], ) + /** + * Lookup'ı ekranın kendi kayıt kaynağından beslenen kolonda (`isLookupSelfSourced`, sunucu + * hesaplar) kaydedilen yeni değer, açılışta yüklenen lookup listesinde olmadığı için hücrede ve + * editörün açılır listesinde görünmez. Başka kaynaktan beslenen lookup kayıtla değişmez; kontrol + * yalnızca bu kolonlarda yapılır ve istek yalnızca kaydedilen değer listede yoksa atılır. + * Bir kolon yenilendiyse `true` döner. + */ + const refreshLookupsForRow = useCallback( + async (instance: any, rowData?: Record) => { + if (!instance?.columnOption || !rowData || !gridDto) return false + + const getCachePrefix = (colData: ColumnFormatDto) => { + const { lookupDto } = colData + if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) { + return `query:${listFormCode}:${colData.fieldName}:` + } + if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) { + return `api:${lookupDto.lookupQuery}:` + } + return undefined + } + + const updated = await Promise.all( + gridDto.columnFormats + .filter( + (colData) => + colData.fieldName && + colData.lookupDto?.dataSourceType && + !colData.lookupDto.cascadeParentFields && + colData.isLookupSelfSourced && + rowData[colData.fieldName] !== undefined && + rowData[colData.fieldName] !== null && + rowData[colData.fieldName] !== '', + ) + .map(async (colData) => { + const prefix = getCachePrefix(colData) + if (!prefix) return + + const source = lookupDataSource({}, colData, listFormCode) + const store = + source instanceof CustomStore + ? source + : (source as { store?: CustomStore } | undefined)?.store + const items = ((await store?.load?.()) ?? []) as Record[] + const valueExpr = colData.lookupDto.valueExpr?.toLowerCase() + const knownValues = new Set( + items.map((item) => { + const key = Object.keys(item).find((itemKey) => itemKey.toLowerCase() === valueExpr) + return key ? String(item[key]) : undefined + }), + ) + + const rawValue = rowData[colData.fieldName!] + const values = Array.isArray(rawValue) ? rawValue : String(rawValue).split('|') + if (values.every((value) => knownValues.has(String(value)))) return + + clearLookupCacheByPrefix(prefix) + // `columnOption('lookup.dataSource', …)` kolonu yeniden kursa da DevExtreme yalnızca + // valueMap'i boş lookup'ları yükler; hücre metnini tutan valueMap ancak lookup'ın + // kendi update()'i ile yenilenir. CardView'da DevExtreme lookup'ı yoktur; metin + // haritasını çağıran taraf yeniden kurar. + const lookup = instance.columnOption(colData.fieldName, 'lookup') + if (lookup?.update) await lookup.update() + return true + }), + ) + + const changed = updated.some(Boolean) + if (changed) instance.repaint?.() + return changed + }, + [gridDto, listFormCode, lookupDataSource], + ) + const createLookupStaticDataSource = ( load: () => any, filter: any = null, @@ -901,6 +979,7 @@ const useListFormColumns = ({ return { getBandedColumns, loadLookupDisplayValues, + refreshLookupsForRow, } } diff --git a/ui/src/views/list/useListFormCustomDataSource.ts b/ui/src/views/list/useListFormCustomDataSource.ts index fed0e0b0..cd2dfeb0 100644 --- a/ui/src/views/list/useListFormCustomDataSource.ts +++ b/ui/src/views/list/useListFormCustomDataSource.ts @@ -374,11 +374,13 @@ const useListFormCustomDataSource = ({ } const updateUrl = getServiceAddress(gridOptions.updateServiceAddress) + // DevExtreme, update'in döndürdüğü nesneyi onRowUpdated'ın `data`'sı yapar ve + // refreshMode reshape/repaint'te store'a iter; axios yanıtı değil gönderilen değerler dönmeli. return dynamicFetch(updateUrl, 'POST', searchParams, { keys: [toPrimaryKey(key)], data: values, listFormCode, - }) + }).then(() => values) }, insert: function (values) { if (!gridOptions.insertServiceAddress) { diff --git a/ui/src/views/shared/lookupCache.ts b/ui/src/views/shared/lookupCache.ts index 25a3a022..fbdf12f6 100644 --- a/ui/src/views/shared/lookupCache.ts +++ b/ui/src/views/shared/lookupCache.ts @@ -65,3 +65,10 @@ export const clearLookupCache = (key?: string) => { lookupCache.delete(key) } } + +/** Anahtarı verilen önekle başlayan girdileri (ör. bir kolonun bütün filtre varyantları) temizler. */ +export const clearLookupCacheByPrefix = (prefix: string) => { + Array.from(lookupCache.keys()) + .filter((key) => key.startsWith(prefix)) + .forEach((key) => lookupCache.delete(key)) +}