Postggre güncellemeleri ve colorbox

This commit is contained in:
Sedat ÖZTÜRK 2026-10-06 14:24:30 +03:00
parent 68dd9b28e6
commit 35118ab2d9
134 changed files with 6203 additions and 1694 deletions

View file

@ -308,9 +308,13 @@ designer. Aggregations available in the Group By column: `GroupBy`, `Where`, `SU
`COUNT_DISTINCT`, `AVG`, `MIN`, `MAX`.
Persistence: only the **table designer's** deploy writes a seed file automatically
(`{sql|postgres}/object/{TableName}.sql`, full CREATE script, via `save-table-script`). View
(`sql/object/{TableName}.sql`, full CREATE script, via `save-table-script`; its PostgreSQL counterpart
`postgres/object/{TableName}.sql` is generated from it by `PostgreSqlTableScript`). View
Designer deploys and any DDL run from the SQL editor (views, procedures, functions) reach the
database only — their seed files under `{sql|postgres}/object/` must be written explicitly.
Every seed script is written for **both** providers: a `sql/…/X.sql` change always comes with its
`postgres/…/X.sql` counterpart (schema `public`, same name and parameter order) — the platform runs on
SQL Server and PostgreSQL alike (`lowcode.instructions.md` §8.3.1, CLAUDE.md standing default 16).
Full mechanics and the SP/function production patterns: `lowcode.instructions.md` §8.1–§8.2.1
and §9.5.

View file

@ -122,6 +122,14 @@ Ek kurallar:
- **Repository**: özel sorgu gerekiyorsa Domain'de arayüz + EntityFrameworkCore'da
implementasyon. AppService içinde `DbContext` doğrudan kullanılmaz.
- **Dinamik SQL**: parametreli çalıştırılır; string birleştirme ile sorgu kurulmaz.
- **İki sağlayıcı**: uygulama SQL Server ve PostgreSQL'de çalışır (`DefaultDatabaseProvider`).
EF modeli SQL Server sözdizimiyle yazılır (`HasColumnType("nvarchar(max)")`,
`HasFilter("[IsDeleted] = 0")`, `HasCheckConstraint`, `HasDefaultValueSql("NEWID()")`);
PostgreSQL'de `PlatformDbContext.ConfigureProviderSpecificModel` bunları `PostgreSqlModelSyntax`
ile çevirir. Çeviride karşılığı olmayan bir sözdizimi kullanılırsa `PostgreSqlModelSyntax`'a
eklenir; immutable olmayan hesaplanan kolon (`GETDATE()`'e bağlı) PG'de yazılamaz, sağlayıcıya
göre dallanır. C#'ta üretilen SQL tırnaklı tanımlayıcı ve ANSI ifadelerle yazılır ya da
`DataSourceType`'a göre dallanır; yalnızca T-SQL'de çalışan metin eklenmez.
- **Mapping**: Mapperly (`[Mapper]` partial class) kullanılır; AutoMapper eklenmez.
Mapper'lar servis klasöründeki `{Alan}Mappers.cs` dosyasındadır (modülde
`{Modül}ApplicationMappers.cs`), ABP'nin `MapperBase<TSource, TDest>` /

View file

@ -50,7 +50,7 @@ ve onay, en son dosyalar.**
configs/seeds/
├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server)
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (her zaman, §8.3.1)
├── {sql|postgres}/host/object|execute/… ← yalnızca host veritabanında çalışan script (ör. HangFire'a dayanan view)
├── crud/{EntityName}.json ← tablonun REST uçları
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları (List yolunda sunucu yazar, §4.1)
@ -509,7 +509,7 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
| Araç | Ne yapar | Not |
| --- | --- | --- |
| `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla (`4.3`) ya da başlık metniyle istenir; aynı numarayı iki başlık taşımamalıdır. Bölüm önce istenen belgede, sonra diğerinde aranır; başlık olmayan bir terim (`dxImageUpload`) terimin geçtiği bölümlerin listesini döndürür |
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `create_table` | Yeni tabloyu kurar (SQL Server ya da PostgreSQL veri kaynağı), `sql/object/{Tablo}.sql` ve PG eşi `postgres/object/{Tablo}.sql` dosyalarını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — alt form olarak üretilen ekran hariç (`Widgets: []`) — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. Talep bağı (`DevRequest`) da yazılmaz — sunucu etkin talepten doldurur (§4.0). İstek demo veri taşıyorsa `SeedData: true` + `seedRows` (§0.10.1). Satır/toolbar butonları `CommandColumns` (§4.9). Bağımlı lookup öğedeki `Cascade*` alanları (§4.3.4). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır; model migrate'i beklemez |
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
@ -1399,6 +1399,14 @@ gibi yüklendiği için öğe anahtarları **küçük harf** (`key`, `name`, cas
`name` değeri `::` öneki eklenerek çevrilir — dil anahtarı da yazılabilir. `Query` sonucunda ise
istemci `Key`/`Name`/`Group` kolonlarını `key`/`name`/`group`'a eşler; sorgu kolonları bu adlarla
döndürür (`SELECT Id AS [Key], Name AS [Name] …`) ve `ValueExpr`/`DisplayExpr` `Key`/`Name` kalır.
Lookup öğesi renk taşıyorsa (etiket tanımındaki `dxColorBox` kolonu gibi) sorgu onu `AS [Color]`
takma adıyla döndürür (`StaticData`'da `color`); `dxTagBox` etiketleri formda ve grid hücresinde bu
renkle çizilir (`views/shared/lookupColor.tsx`), renk yoksa varsayılan rozet rengi kalır. Dinamik
raporlar (`DynamicGrid`/`DynamicTree`/`DynamicForm`) `Query` lookup'ında takma adı görünce rapor
SQL'ine `{Alan}__LookupColor` kolonu ekler ve değeri renkli rozet olarak basar
(`DynamicReportColorHelper`); sorgu rapor kurulurken çalıştırılmadığı için takma ad yazılmazsa
rapor düz metin kalır. `dxColorBox` editörlü kolon grid'de ve raporlarda değerin yanında renk
örneğiyle gösterilir.
`LookupDataSourceType` `1` (StaticData) ise `LookupQuery` bir JSON dizisi, `2` (Query) ise SQL,
`3` (WebService) ise noktalı virgülle ayrılmış bir çağrı tarifidir. Tam sözleşme ve cascade
@ -3564,8 +3572,8 @@ tabloları için yalnız `T`/`D` üretir.)
1. Üretilen SQL hedef veri kaynağında çalıştırılır (yeni tabloda `CREATE`, düzenleme modunda
**`ALTER` diff'i**).
2. Seed dosyası kaydedilir: her zaman **tam `CREATE TABLE` script'i** (diff değil),
`save-table-script` ucu ile `configs/seeds/{sql|postgres}/object/{TabloAdı}.sql`
altına (`SeedPathResolver`). Dosya kaydı başarısız olsa deploy geri alınmaz — log'a düşer.
`save-table-script` ucu ile `configs/seeds/sql/object/{TabloAdı}.sql` altına; PostgreSQL eşi
aynı script'ten `PostgreSqlTableScript` ile üretilip `postgres/object/{TabloAdı}.sql` altına yazılır. Dosya kaydı başarısız olsa deploy geri alınmaz — log'a düşer.
3. CRUD adımında işaretlendiyse endpoint'ler üretilir (`crud/{Entity}.json`); onay kutusu
**varsayılan kapalıdır**, Custom Component'in kullanacağı tabloda açılır.
@ -3656,10 +3664,10 @@ vardır; seed dosyası yazarken aynı iskeleti kullan ve §8.3'ün idempotency k
| --- | --- |
| Dosya adı | Nesne adıyla **birebir aynı**: `Hr_T_AdvanceRequest.sql` |
| Klasör | `object` = idempotent nesne tanımı · `execute` = nesne olarak da oluşturulur, ayrıca tüm migration'lar bittikten sonra çalıştırılır |
| Sağlayıcı | Hedef SQL Server ise `sql/`, PostgreSQL ise `postgres/`. **İkisini birden destekliyorsan iki dosya da yazılır**; içerikler aynı nesnenin iki diyalektidir. |
| İdempotency | SQL Server: `CREATE OR ALTER VIEW/PROCEDURE/FUNCTION`, tablolar için `IF NOT EXISTS` koruması. PostgreSQL: `CREATE OR REPLACE VIEW/FUNCTION`, `CREATE TABLE IF NOT EXISTS`. |
| Şema | `dbo` kullanılır; PostgreSQL tarafında tanımlayıcılar çift tırnaklıdır (`dbo."Hr_T_AdvanceRequest"`) çünkü kolon adları PascalCase'tir. |
| Toplu ayraç | SQL Server'da prosedür sonunda `GO` kullanılabilir; PostgreSQL'de kullanılmaz. |
| Sağlayıcı | **Her nesne iki dosyadır**: `sql/…/X.sql` (SQL Server) ve `postgres/…/X.sql` (PostgreSQL), aynı göreli yol ve ad. Biri yazılıp/değiştirilip diğeri bırakılmaz (§8.3.1). |
| İdempotency | SQL Server: `CREATE OR ALTER VIEW/PROCEDURE/FUNCTION`, tablolar için `IF NOT EXISTS` koruması. PostgreSQL: `CREATE OR REPLACE VIEW/FUNCTION/PROCEDURE`, `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, constraint için `pg_constraint` kontrollü `DO $$` bloğu. |
| Şema | SQL Server'da `dbo`; PostgreSQL'de `public` (EF tabloları oradadır, `dbo` şeması yoktur). PG'de tanımlayıcılar çift tırnaklıdır (`public."Hr_T_AdvanceRequest"`, `"TenantId"`) çünkü adlar PascalCase'tir. |
| Toplu ayraç | SQL Server'da batch'ler `GO` ile ayrılır. PostgreSQL dosyasında `GO` gerekmez; dosya tek komut metni olarak da çalışır. |
| Yorum | Dosyanın ilk satırında ne ürettiğini yaz (`-- Create View`). |
Tablo yazarken §8.1'deki varsayılan kolonlar dahil edilir. Örnek iskelet (SQL Server):
@ -3686,14 +3694,37 @@ END
> `Id` tipini ne seçtiysen wizard dosyasındaki `KeyFieldDbSourceType` ile aynı olmalıdır
> (`UNIQUEIDENTIFIER` → `9`, `INT IDENTITY` → `11`). Uyumsuzluk sessiz veri hatası üretir.
#### 8.3.1 PostgreSQL eşi
Uygulama iki sağlayıcıda da çalışır; `sql/` altına yazılan ya da değiştirilen her dosyanın
`postgres/` eşi **aynı işte** yazılır (CLAUDE.md standing default 16). Eş, aynı nesnenin aynı
davranışla PG diyalektidir: aynı satırları yazar, aynı doğrulamayı yapar, aynı (Türkçe) mesajı verir.
| Konu | Kural |
| --- | --- |
| Rutin dili | Fonksiyon ve prosedürler `LANGUAGE plpgsql`. Dosyalar alfabetik çalıştığı için `Hr_S_*` tablolardan (`Hr_T_*`) önce kurulur; `plpgsql` gövdesi kurulumda doğrulanmaz, `LANGUAGE sql` doğrulanır ve düşer. |
| Rutin türü | Skaler/tablo fonksiyonu → `FUNCTION … RETURNS <tip>` / `RETURNS TABLE(…)`. Sonuç kümesi döndürmeyen prosedür → `PROCEDURE` (çağrı `CALL`). Sonunda satır döndüren T-SQL prosedürü → `FUNCTION … RETURNS TABLE` (PG prosedürü sonuç kümesi döndüremez). `execute/` dosyası her zaman `PROCEDURE`'dür (`FinalSeeder` yalnızca `prokind = 'p'` çağırır). Zamanlanmış işin `BeforeSp`'si (`SpRepository`) PG'de katalogdan türe bakar: prosedür `CALL`, fonksiyon `SELECT * FROM` ile, ad tırnaklı `public."Ad"` olarak çağrılır. |
| Parametre | Ad ve **sıra** T-SQL ile aynı (çağıranlar sırayla geçer); `@Ad` → `p_Ad`, yerel değişken `v_Ad` — kolon adlarıyla belirsizlik olmasın diye. |
| Transaction | `BEGIN TRAN/COMMIT`, `SET NOCOUNT/XACT_ABORT` yazılmaz; çağıranın transaction'ı atomikliği sağlar. |
| Hata | `THROW 51100, N'Mesaj', 1` → `RAISE EXCEPTION 'Mesaj' USING ERRCODE = 'P0001', DETAIL = '51100'` (mesajdaki `%` → `%%`). |
| Tipler | `uniqueidentifier`→`uuid`, `nvarchar(n)`→`varchar(n)`, `nvarchar(MAX)`→`text`, `bit`→`boolean`, `datetime2`→`timestamp`, `decimal`→`numeric`, `int`→`integer`, `tinyint`→`smallint`, `float`→`double precision`. |
| Varsayılan | `NEWID()`→`gen_random_uuid()`, `GETDATE()`→`LOCALTIMESTAMP`, `GETUTCDATE()`→`(NOW() AT TIME ZONE 'UTC')`, bit `0/1`→`FALSE/TRUE`. |
| Boolean | `bit` kolon karşılaştırması `= 0/1` değil `= FALSE/TRUE` (check ve index filtresi dahil). |
| İfadeler | `ISNULL`→`COALESCE`, `TOP n`→`LIMIT n`, `+` (metin)→`\|\|`, `OUTER/CROSS APPLY`→`LEFT/CROSS JOIN LATERAL`, `DATEADD/DATEDIFF`→interval/tarih farkı, `CHARINDEX`→`strpos`, `STRING_SPLIT`→`string_to_table`, `@@ROWCOUNT`→`GET DIAGNOSTICS`, tablo değişkeni/`#temp`→`CREATE TEMP TABLE … ON COMMIT DROP`, `MERGE`→`INSERT … ON CONFLICT`. |
| Ad uzunluğu | PG tanımlayıcısı en fazla 63 karakterdir; uzun constraint/index adı sessizce kısalır ve varlık kontrolü bir dahaki migrate'te tutmaz. Adı 63 karakterin altında tut. |
| Büyük/küçük harf | SQL Server karşılaştırması harf duyarsızdır, PG duyarlıdır; kullanıcı girdisi/ad karşılaştırmasında `lower()`/`ILIKE` kullan. |
Doğrulama: PG eşi bir PostgreSQL veritabanında **iki kez** çalıştırılır (idempotent) ve her rutin
gerçekçi veriyle en az bir kez çağrılır — `plpgsql` tip/belirsiz kolon hatalarını yalnızca çalışırken verir.
### 8.4 Seeder bu dosyaları nasıl çalıştırır
Yeni bir entity/tablo `sql/object/` altına yazılır ve veritabanı `SqlDataSeeder` tarafından bu
dosyalardan kurulur. Mekanik:
1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise
`sql/`, PostgreSQL ise `postgres/`. İki dosya da yazmak taşınabilirlik
içindir; aynı anda ikisi birden çalışmaz.
`sql/`, PostgreSQL ise `postgres/`. Aynı anda ikisi birden çalışmaz ama iki dosya da her
zaman yazılır (§8.3.1); yazılmayan eş, o sağlayıcıdaki kurulumu ilk migrate'te kırar.
2. Sıra: `object/` → `execute/`, her klasörün içinde **dosya adına göre alfabetik**. Bir tablo
başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql`
`Hr_T_EmployeeAdvance.sql`'den önce gelir).
@ -3727,7 +3758,7 @@ içindir.
### 8.5 Yeni entity/tablo eklerken izlenecek yol
1. `sql/object/{Modul}_T_{Entity}.sql` — tabloyu §8.3 iskeletiyle yaz (tenant + audit kolonları
varsayılan). PostgreSQL hedefi de destekleniyorsa `postgres/object/` altına diyalekt eşini yaz.
varsayılan) ve `postgres/object/` altına diyalekt eşini yaz (§8.3.1).
2. İndeks ve FK'leri **aynı dosyada**, varlık kontrolüyle ekle (`IF NOT EXISTS`); ayrı dosya
sıralamayı kırılgan hâle getirir.
3. Ekran bu tabloyu doğrudan kullanacaksa CRUD endpoint gerekmez — ListForm `SelectCommandType: 1`
@ -4689,7 +4720,7 @@ Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep fo
```
configs/seeds/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ postgres/object/… PG eşi, §8.3.1)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── data/App.DeveloperKit.CustomComponents/{Id}.json ← yalnızca serbest yerleşim isteniyorsa
├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt

View file

@ -230,6 +230,10 @@ Uygulama `https://localhost:44344` adresinde açılır. Swagger UI kök adreste
- `GET /api/setup/application-status` — veritabanının hazır olup olmadığını döner (anonim).
- UI tarafında `/setup` route'u (`views/setup/DatabaseSetup.tsx`) migration'ı tetikleyen ekranı gösterir.
- Migration için `Setup:MigrationPassword` yapılandırması veya `App.Setup.Migrate` yetkisi gerekir.
- Ekran DbMigrator'ü `SetupMigrationRunner` ile çalıştırır. `Setup:MigratorPath` publish çıktısını
gösteriyorsa DLL doğrudan çalışır; kaynak proje klasöründe (geliştirme) `dotnet run` kullanılmaz —
`bin/Debug` altındaki DLL `api/` kaynaklarından (ve son başarılı derlemenin `obj/setup-migrator.stamp`
damgasından) yeniyse derleme atlanır, değilse `dotnet build` çıktısı ekrandaki loga akar.
Alternatif olarak migration + seed'i doğrudan çalıştırabilirsiniz:
@ -280,7 +284,12 @@ migrate'te bu, kendi veritabanı olan son tenant'tır; öyle bir tenant yoksa ho
kapansaydı, ekranın eski sürümü kurulu olan tenant ekranı "kurulu" sayıp güncellemezdi. Tek
tenant seed'i (Tenants › Seed) bayrağı kapatmaz.
Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/` okunur. Her
Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/` okunur. İki klasör
birebir eştir: her `sql/…/X.sql` dosyasının aynı adlı `postgres/…/X.sql` karşılığı vardır (PG'de şema
`public`; çeviri kuralları `lowcode.instructions.md` §8.3.1). EF modeli SQL Server sözdizimiyle
yazılır, PostgreSQL'de `PostgreSqlModelSyntax` ile çevrilir (`PlatformDbContext.ConfigureProviderSpecificModel`);
`Order.IsActive` SQL Server'da lisans bitişinden hesaplanan kolondur, PG'de hesaplanan kolon
`CURRENT_DATE` alamadığı için saklanan kolondur (varsayılan `true`). Her
`.sql` dosyası amacını belirten bir klasörün altındadır; klasör köklerinde dosya bulunmaz:
| Klasör | Ne zaman çalışır |
@ -1558,7 +1567,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. |
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`, ya da başlık metniyle). Bölüm önce istenen, sonra diğer belgede aranır; başlıkla eşleşmeyen terimde (örn. `dxImageUpload`) terimin geçtiği bölümler (en çok 15, `DevGuideSearchMatches`), hiç geçmiyorsa başlık listesi döner — hata atılmaz. Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `sql/object/{Tablo}.sql` dosyasına, PostgreSQL eşi (`PostgreSqlTableScript`) `postgres/object/{Tablo}.sql` dosyasına yazılır; PG veri kaynağında çalıştırılan da o eştir. Yalnızca oluşturur — tablo varsa reddeder. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |

View file

@ -76,5 +76,4 @@ public static class McpErrorCodes
public const string InvalidDefaultValue = Prefix + "InvalidDefaultValue";
public const string DuplicateColumn = Prefix + "DuplicateColumn";
public const string TableAlreadyExists = Prefix + "TableAlreadyExists";
public const string TableProviderNotSupported = Prefix + "TableProviderNotSupported";
}

View file

@ -17,7 +17,7 @@ namespace Sozsoft.Mcp.Services;
/// <summary>
/// Yeni tabloyu SQL Query Manager'daki tablo tasarimcisiyla ayni sozlesmeyle kurar: script
/// tasarimcinin <c>generateCreateTableSql</c> ciktisiyla ayni bicimdedir ve tasarimcinin deploy
/// sonrasi yazdigi yere (<c>{kapsam}/sql/object/{Tablo}.sql</c>) yazilir; boylece tablo
/// sonrasi yazdigi yerlere (<c>sql/object/{Tablo}.sql</c> ve PostgreSQL esi <c>postgres/object/{Tablo}.sql</c>) yazilir; boylece tablo
/// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir.
/// <para>
/// Yalnizca olusturma yapar. Var olan tabloya dokunulmaz — kolon ekleme/degistirme tasarimcinin
@ -95,29 +95,32 @@ public class McpTableCreator(
var columns = BuildColumns(definition);
var context = await queryExecutor.ResolveDataSourceAsync();
if (context.DataSourceType != DataSourceTypeEnum.Mssql)
{
throw new UserFriendlyException(localizer[McpErrorCodes.TableProviderNotSupported, context.DataSourceType.ToString()]);
}
var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql;
var existing = await context.Repository.ExecuteScalarAsync<int?>(
"SELECT OBJECT_ID(@name, 'U')",
var existing = await context.Repository.ExecuteScalarAsync<object?>(
isPostgreSql ? "SELECT to_regclass(@name)::text" : "SELECT OBJECT_ID(@name, 'U')",
context.ConnectionString,
new Dictionary<string, object> { ["name"] = $"{SchemaName}.{tableName}" });
new Dictionary<string, object> { ["name"] = isPostgreSql ? $"public.\"{tableName}\"" : $"{SchemaName}.{tableName}" });
if (existing is not null)
if (existing is not null and not DBNull)
{
throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]);
}
// Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir ve PG veri
// kaynağında çalıştırılan da o eştir.
var script = BuildScript(tableName, columns);
if (!PostgreSqlTableScript.TryConvert(script, out var postgreSqlScript))
{
throw new InvalidOperationException($"PostgreSQL script could not be generated for {tableName}.");
}
// GO yalnizca SSMS ayiricidir; tek batch oldugu icin calistirirken atilir, dosyada kalir.
await context.Repository.ExecuteAsync(
string.Join('\n', script.Split('\n').Where(line => line.Trim() != "GO")),
isPostgreSql ? postgreSqlScript : string.Join('\n', script.Split('\n').Where(line => line.Trim() != "GO")),
context.ConnectionString);
var seedFilePath = SaveSeedScript(tableName, script);
var (seedFilePath, _) = PostgreSqlTableScript.WriteSeedFiles(configuration, tableName, script);
return new McpCreatedTable(tableName, script, seedFilePath);
}
@ -264,25 +267,6 @@ public class McpTableCreator(
return $" [{column.Name}] {type} {nullPart}{defaultPart}";
}
/// <summary>
/// Tasarimcinin <c>SaveTableScriptAsync</c> ile yazdigi yol.
/// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir.
/// </summary>
private string SaveSeedScript(string tableName, string script)
{
var outputPath = SeedPathResolver.GetPath(
configuration,
SeedPathResolver.GetProviderFolderName(isPostgreSql: false),
SeedPathResolver.ObjectFolder);
Directory.CreateDirectory(outputPath);
var filePath = Path.Combine(outputPath, $"{tableName}.sql");
File.WriteAllText(filePath, script);
return $"{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{tableName}.sql";
}
private void EnsureSafeIdentifier(string identifier)
{
if (!SafeIdentifierRegex.IsMatch(identifier))

View file

@ -671,14 +671,15 @@ FROM (
try
{
var outputPath = ResolveSqlDataOutputPath();
Directory.CreateDirectory(outputPath);
// Tasarımcı scripti T-SQL'dir; uygulama iki sağlayıcıda da kurulabildiği için PostgreSQL
// eşi aynı anda yazılır (postgres/object).
var safeFileName = string.Concat(input.FileName.Trim().Split(Path.GetInvalidFileNameChars()));
var filePath = Path.Combine(outputPath, $"{safeFileName}.sql");
File.WriteAllText(filePath, input.SqlScript);
var (sqlServerPath, postgreSqlPath) = PostgreSqlTableScript.WriteSeedFiles(_configuration, safeFileName, input.SqlScript);
_logger.LogInformation("SQL seed file saved: {FilePath}", filePath);
_logger.LogInformation("SQL seed file saved: {FilePath}", sqlServerPath);
if (postgreSqlPath is null)
_logger.LogWarning("PostgreSQL counterpart of {FilePath} could not be generated; write it by hand.", sqlServerPath);
}
catch (Exception ex)
{
@ -771,20 +772,15 @@ FROM (
{
try
{
var outputPath = ResolveSqlDataOutputPath();
if (!Directory.Exists(outputPath))
return;
var safeFileName = string.Concat(fileName.Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeFileName))
return;
var filePath = Path.Combine(outputPath, $"{safeFileName}.sql");
if (!File.Exists(filePath))
return;
File.Delete(filePath);
_logger.LogInformation("SQL object script deleted: {FilePath}", filePath);
foreach (var filePath in ResolveSeedObjectPaths().Select(path => Path.Combine(path, $"{safeFileName}.sql")).Where(File.Exists))
{
File.Delete(filePath);
_logger.LogInformation("SQL object script deleted: {FilePath}", filePath);
}
}
catch (Exception ex)
{
@ -922,10 +918,6 @@ FROM (
try
{
var outputPath = ResolveSqlDataOutputPath();
if (!Directory.Exists(outputPath))
return Task.CompletedTask;
foreach (var rawName in input.FileNames.Distinct())
{
if (string.IsNullOrWhiteSpace(rawName))
@ -939,12 +931,11 @@ FROM (
if (string.IsNullOrWhiteSpace(safeFileName))
continue;
var filePath = Path.Combine(outputPath, $"{safeFileName}.sql");
if (!File.Exists(filePath))
continue;
File.Delete(filePath);
_logger.LogInformation("SQL seed file deleted: {FilePath}", filePath);
foreach (var filePath in ResolveSeedObjectPaths().Select(path => Path.Combine(path, $"{safeFileName}.sql")).Where(File.Exists))
{
File.Delete(filePath);
_logger.LogInformation("SQL seed file deleted: {FilePath}", filePath);
}
}
}
catch (Exception ex)
@ -971,4 +962,14 @@ FROM (
return SeedPathResolver.GetPath(_configuration, providerDirectoryName, SeedPathResolver.ObjectFolder);
}
/// <summary>
/// İki sağlayıcının <c>object</c> klasörü. Düşürülen nesnenin dosyası ikisinden de silinir; yalnızca
/// aktif sağlayıcıdan silinseydi diğer sağlayıcıdaki kurulum nesneyi yeniden yaratırdı.
/// </summary>
private string[] ResolveSeedObjectPaths() =>
[
SeedPathResolver.GetPath(_configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder),
SeedPathResolver.GetPath(_configuration, SeedPathResolver.PostgresFolder, SeedPathResolver.ObjectFolder),
];
}

View file

@ -177,13 +177,19 @@ public class WizardDataSeeder(
seedFile.ListForm.ApplyTo(listForm);
await repoListForm.InsertAsync(listForm, autoSave: true);
foreach (var fieldDto in seedFile.Fields)
// Alanlar ve is akisi tek SaveChanges ile yazilir; satir basina kayit, izlenen varlik sayisi
// buyudukce her kayitta degisiklik taramasini tekrarlar ve seed suresini katlar.
var fields = seedFile.Fields.Select(fieldDto =>
{
var field = new ListFormField();
fieldDto.ApplyTo(field);
await repoListFormField.InsertAsync(field, autoSave: true);
}
return field;
}).ToList();
if (fields.Count > 0)
await repoListFormField.InsertManyAsync(fields, autoSave: true);
var workflows = new List<ListFormWorkflow>();
foreach (var criteria in seedFile.ListForm.WorkflowDto.Criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
@ -192,7 +198,7 @@ public class WizardDataSeeder(
continue;
}
await repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
workflows.Add(new ListFormWorkflow(criteria.Id)
{
ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? listFormCode : criteria.ListFormCode,
Kind = criteria.Kind,
@ -209,8 +215,11 @@ public class WizardDataSeeder(
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
}, autoSave: true);
});
}
if (workflows.Count > 0)
await repoListFormWorkflow.InsertManyAsync(workflows, autoSave: true);
}
/// <summary>

View file

@ -1,6 +1,5 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.Autofac;
using Volo.Abp.Caching;
@ -22,9 +21,10 @@ public class PlatformDbMigratorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostEnvironment = context.Services.GetSingletonInstance<IHostEnvironment>();
ConfigureVirtualFileSystem(context, hostEnvironment);
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<PlatformDbMigratorModule>();
});
Configure<AbpDistributedCacheOptions>(options => { options.KeyPrefix = "Platform:"; });
var configuration = context.Services.GetConfiguration();
@ -35,21 +35,6 @@ public class PlatformDbMigratorModule : AbpModule
options.ConnectionStrings.Default = connectionString;
});
}
private void ConfigureVirtualFileSystem(ServiceConfigurationContext context, IHostEnvironment hostEnvironment)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<PlatformDbMigratorModule>();
// if (hostEnvironment.IsDevelopment())
// {
// options.FileSets.ReplaceEmbeddedByPhysical<PlatformDbMigratorModule>(
// Path.Combine(hostEnvironment.ContentRootPath,
// // $"..{Path.DirectorySeparatorChar}Sozsoft.Platform.DbMigrator"));
// $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}"));
// }
});
}
}

View file

@ -333,7 +333,7 @@ public class ListFormSeedDataApplier(
listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType,
overwriteExisting, existingKeys, batch);
if (batch.ParameterCount >= InsertBatch.MaxParameterCount)
if (batch.IsFull)
await batch.FlushAsync(repository, connectionString);
switch (outcome)
@ -508,10 +508,16 @@ public class ListFormSeedDataApplier(
/// <c>@p{sira}_{ad}</c> olarak yeniden adlandirilir; boylece ekranin kendi insert komutu
/// (<c>InsertCommand</c>) da degismeden toplu calisir. Komut basina parametre sayisi SQL Server
/// sinirinin (2100) altinda tutulur.
/// <para>
/// Satir sayisi da sinirlidir: komutun derlenme suresi satir sayisiyla buyur ve bos tablo
/// doldukca istatistik guncellemesi plani tekrar tekrar yeniden derletir. 400 satirlik komutla
/// 12 bin dil metni ~25 sn, 25 satirlik komutla ~4 sn surer.
/// </para>
/// </summary>
private sealed class InsertBatch
{
public const int MaxParameterCount = 2000;
private const int MaxParameterCount = 2000;
private const int MaxRowCount = 25;
private static readonly Regex ParameterRegex = new(@"(?<![@\w])@(\w+)", RegexOptions.Compiled);
@ -519,7 +525,7 @@ public class ListFormSeedDataApplier(
private readonly Dictionary<string, object> parameters = [];
private int rowCount;
public int ParameterCount => parameters.Count;
public bool IsFull => rowCount >= MaxRowCount || parameters.Count >= MaxParameterCount;
public void Add(string rowSql, Dictionary<string, object> rowParameters)
{

View file

@ -0,0 +1,366 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Tablo tasarımcısının ve MCP <c>create_table</c>'ın ürettiği SQL Server tablo scriptini
/// (<c>sql/object/{Tablo}.sql</c>) aynı tablonun PostgreSQL eşine (<c>postgres/object/{Tablo}.sql</c>)
/// çevirir. Uygulama iki sağlayıcıda da çalıştığı için uygulama içinden yazılan her tablo scripti
/// iki dosya olarak yazılır; elle yazılan seed dosyasıyla aynı kurala uyar (idempotent, şema
/// <c>public</c>, constraint <c>pg_constraint</c> kontrollü).
/// <para>
/// Yalnızca tablo scripti biçimi tanınır: <c>CREATE TABLE</c>, <c>ADD CONSTRAINT</c> (CHECK / UNIQUE /
/// FOREIGN KEY) ve <c>CREATE INDEX</c> batch'leri. Görünüm, prosedür ve fonksiyon gövdesi diyalektler
/// arasında güvenle çevrilemez; tanınmayan batch'te <see cref="TryConvert"/> false döner.
/// </para>
/// </summary>
public static partial class PostgreSqlTableScript
{
/// <summary>
/// Tablo scriptini iki sağlayıcının <c>object</c> klasörüne yazar ve seed köküne göreli yollarını
/// döndürür. PostgreSQL eşi çevrilemezse (tanınmayan batch) yalnızca SQL Server dosyası yazılır ve
/// ikinci yol <c>null</c> döner; çağıran bunu bildirir.
/// </summary>
public static (string SqlServerPath, string? PostgreSqlPath) WriteSeedFiles(
IConfiguration? configuration,
string tableName,
string sqlServerScript)
{
var fileName = $"{tableName}.sql";
var sqlServerFolder = SeedPathResolver.GetPath(configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder);
Directory.CreateDirectory(sqlServerFolder);
File.WriteAllText(Path.Combine(sqlServerFolder, fileName), sqlServerScript);
var sqlServerPath = $"{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{fileName}";
if (!TryConvert(sqlServerScript, out var postgreSqlScript))
return (sqlServerPath, null);
var postgreSqlFolder = SeedPathResolver.GetPath(configuration, SeedPathResolver.PostgresFolder, SeedPathResolver.ObjectFolder);
Directory.CreateDirectory(postgreSqlFolder);
File.WriteAllText(Path.Combine(postgreSqlFolder, fileName), postgreSqlScript);
return (sqlServerPath, $"{SeedPathResolver.PostgresFolder}/{SeedPathResolver.ObjectFolder}/{fileName}");
}
public static bool TryConvert(string sqlServerScript, out string postgreSqlScript)
{
postgreSqlScript = string.Empty;
if (string.IsNullOrWhiteSpace(sqlServerScript))
return false;
var script = UseStatementRegex().Replace(sqlServerScript, string.Empty);
var bitColumns = BitColumnRegex().Matches(script)
.Select(m => m.Groups["name"].Value)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var statements = new List<string>();
foreach (var rawBatch in BatchSeparatorRegex().Split(script))
{
var batch = BlockCommentRegex().Replace(rawBatch, " ");
batch = LineCommentRegex().Replace(batch, string.Empty).Trim();
if (batch.Length == 0)
continue;
// Tasarımcı birden çok ALTER/INDEX deyimini aynı batch'e yazar; her biri ayrı çevrilir.
foreach (var statement in SplitStatements(batch))
{
var converted = ConvertStatement(statement, bitColumns);
if (converted is null)
return false;
statements.Add(converted);
}
}
if (statements.Count == 0)
return false;
postgreSqlScript = string.Join("\n\n", statements) + "\n";
return true;
}
/// <summary>
/// Batch'i deyimlere ayırır: <c>IF … BEGIN … END</c> koruması kaldırılır, kalan metin
/// <c>ALTER TABLE</c> / <c>CREATE TABLE</c> / <c>CREATE INDEX</c> başlangıçlarından bölünür.
/// </summary>
private static IEnumerable<string> SplitStatements(string batch)
{
var body = GuardRegex().Replace(batch, string.Empty);
body = BeginEndRegex().Replace(body, " ");
return StatementStartRegex()
.Split(body)
.Select(a => a.Trim().TrimEnd(';').Trim())
.Where(a => a.Length > 0);
}
private static string? ConvertStatement(string statement, HashSet<string> bitColumns)
{
if (statement.StartsWith("CREATE TABLE", StringComparison.OrdinalIgnoreCase))
return ConvertCreateTable(statement);
var constraint = AddConstraintRegex().Match(statement);
if (constraint.Success)
{
var table = Identifier(constraint.Groups["table"].Value);
var name = constraint.Groups["name"].Value;
var definition = ConvertConstraintDefinition(constraint.Groups["definition"].Value.Trim(), bitColumns);
return "DO $$\nBEGIN\n" +
$" IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '{name.Replace("'", "''")}') THEN\n" +
$" ALTER TABLE {table} ADD CONSTRAINT {Quote(name)} {definition};\n" +
" END IF;\nEND\n$$;";
}
var index = CreateIndexRegex().Match(statement);
if (index.Success)
{
var sql = new StringBuilder("CREATE ");
if (index.Groups["unique"].Success)
sql.Append("UNIQUE ");
sql.Append($"INDEX IF NOT EXISTS {Quote(index.Groups["name"].Value)} ON {Identifier(index.Groups["table"].Value)} ({ColumnList(index.Groups["columns"].Value)})");
if (index.Groups["include"].Success)
sql.Append($" INCLUDE ({ColumnList(index.Groups["include"].Value)})");
if (index.Groups["where"].Success)
sql.Append(" WHERE ").Append(Expression(index.Groups["where"].Value.Trim(), bitColumns));
return sql.Append(';').ToString();
}
return null;
}
private static string? ConvertCreateTable(string statement)
{
var match = CreateTableRegex().Match(statement);
if (!match.Success)
return null;
var inner = match.Groups["body"].Value;
var primaryKey = PrimaryKeyRegex().Match(inner);
var columnPart = primaryKey.Success ? inner[..primaryKey.Index] : inner;
var lines = new List<string>();
foreach (var rawLine in columnPart.Split('\n'))
{
if (string.IsNullOrWhiteSpace(rawLine))
continue;
var column = ColumnRegex().Match(rawLine);
if (!column.Success)
return null;
var type = ColumnType(column.Groups["type"].Value);
if (type is null)
return null;
var line = new StringBuilder($" {Quote(column.Groups["name"].Value)} {type}");
if (column.Groups["identity"].Success)
line.Append(" GENERATED BY DEFAULT AS IDENTITY");
if (column.Groups["null"].Success)
line.Append(' ').Append(column.Groups["null"].Value.Trim().ToUpperInvariant());
if (column.Groups["default"].Success)
line.Append(" DEFAULT ").Append(DefaultValue(column.Groups["default"].Value, type));
lines.Add(line.ToString());
}
if (primaryKey.Success)
{
var columns = string.Join(", ", primaryKey.Groups["columns"].Value
.Split(',')
.Select(a => Quote(SortOrderRegex().Replace(a.Trim(), string.Empty).Trim('[', ']', ' '))));
lines.Add($" CONSTRAINT {Quote(primaryKey.Groups["name"].Value)} PRIMARY KEY ({columns})");
}
return $"CREATE TABLE IF NOT EXISTS {Identifier(match.Groups["table"].Value)}\n(\n{string.Join(",\n", lines)}\n);";
}
private static string ConvertConstraintDefinition(string definition, HashSet<string> bitColumns)
{
definition = WhitespaceRegex().Replace(definition, " ");
definition = ClusteringRegex().Replace(definition, string.Empty);
definition = SortOrderInListRegex().Replace(definition, "$1");
return Expression(definition, bitColumns);
}
private static string ColumnList(string columns) =>
string.Join(", ", columns.Split(',')
.Select(a => a.Trim())
.Where(a => a.Length > 0)
.Select(a =>
{
var descending = a.EndsWith(" DESC", StringComparison.OrdinalIgnoreCase);
var name = Quote(SortOrderRegex().Replace(a, string.Empty).Trim('[', ']', ' '));
return descending ? name + " DESC" : name;
}));
/// <summary><c>[x]</c> → <c>"x"</c>, <c>N'..'</c> → <c>'..'</c>, bit kolon <c>= 0/1</c> → <c>= FALSE/TRUE</c>.</summary>
private static string Expression(string expression, HashSet<string> bitColumns)
{
expression = Identifier(expression);
expression = UnicodeLiteralRegex().Replace(expression, "'");
return BitComparisonRegex().Replace(expression, m =>
bitColumns.Contains(m.Groups["column"].Value)
|| m.Groups["column"].Value.StartsWith("Is", StringComparison.Ordinal)
|| m.Groups["column"].Value.StartsWith("Has", StringComparison.Ordinal)
? $"\"{m.Groups["column"].Value}\" = {(m.Groups["value"].Value == "1" ? "TRUE" : "FALSE")}"
: m.Value);
}
/// <summary><c>[dbo].[X]</c> → <c>public."X"</c>, kalan <c>[x]</c> → <c>"x"</c>.</summary>
private static string Identifier(string text)
{
text = DboIdentifierRegex().Replace(text, m => "public." + Quote(m.Groups["name"].Value));
return BracketIdentifierRegex().Replace(text, m => Quote(m.Groups["name"].Value));
}
private static string Quote(string name) => "\"" + name.Replace("\"", "\"\"") + "\"";
private static string? ColumnType(string sqlServerType)
{
var type = WhitespaceRegex().Replace(sqlServerType.Trim().ToLowerInvariant(), string.Empty);
if (type is "nvarchar(max)" or "varchar(max)")
return "text";
var sized = SizedTypeRegex().Match(type);
if (sized.Success)
{
var size = sized.Groups["size"].Value;
return sized.Groups["name"].Value switch
{
"nvarchar" or "varchar" => $"varchar({size})",
"nchar" or "char" => $"char({size})",
"decimal" or "numeric" => $"numeric({size.Replace(",", ", ")})",
_ => null,
};
}
return type switch
{
"uniqueidentifier" => "uuid",
"datetime2" or "datetime" or "smalldatetime" => "timestamp",
"datetimeoffset" => "timestamp with time zone",
"date" => "date",
"time" => "time",
"bit" => "boolean",
"int" => "integer",
"bigint" => "bigint",
"smallint" or "tinyint" => "smallint",
"float" => "double precision",
"real" => "real",
"money" => "numeric(19, 4)",
"varbinary(max)" or "image" => "bytea",
"decimal" => "numeric(18, 0)",
_ => null,
};
}
private static string DefaultValue(string value, string postgreSqlType)
{
var text = value.Trim();
while (text.StartsWith('(') && text.EndsWith(')'))
text = text[1..^1].Trim();
switch (text.ToUpperInvariant())
{
case "NEWID()":
case "NEWSEQUENTIALID()":
return "gen_random_uuid()";
case "GETUTCDATE()":
case "SYSUTCDATETIME()":
return "(NOW() AT TIME ZONE 'UTC')";
case "GETDATE()":
case "SYSDATETIME()":
return "LOCALTIMESTAMP";
}
if (postgreSqlType == "boolean")
return text == "1" ? "TRUE" : "FALSE";
return UnicodeLiteralRegex().Replace(text, "'");
}
[GeneratedRegex(@"^\s*USE\s+\[[^\]]+\]\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase)]
private static partial Regex UseStatementRegex();
[GeneratedRegex(@"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase)]
private static partial Regex BatchSeparatorRegex();
[GeneratedRegex(@"/\*.*?\*/", RegexOptions.Singleline)]
private static partial Regex BlockCommentRegex();
[GeneratedRegex(@"--[^\r\n]*")]
private static partial Regex LineCommentRegex();
[GeneratedRegex(@"^\s*IF\s+(?:NOT\s+EXISTS\s*\((?>[^()]+|\((?<d>)|\)(?<-d>))*\)|(?:OBJECT_ID|INDEXPROPERTY)\s*\((?>[^()]+|\((?<d>)|\)(?<-d>))*\)\s+IS\s+(?:NOT\s+)?NULL)", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
private static partial Regex GuardRegex();
[GeneratedRegex(@"^\s*(?:BEGIN|END)\s*;?\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
private static partial Regex BeginEndRegex();
[GeneratedRegex(@"(?=\bALTER\s+TABLE\b|\bCREATE\s+TABLE\b|\bCREATE\s+(?:UNIQUE\s+)?(?:(?:NON)?CLUSTERED\s+)?INDEX\b)", RegexOptions.IgnoreCase)]
private static partial Regex StatementStartRegex();
[GeneratedRegex(@"^CREATE\s+TABLE\s+(?<table>\S+)\s*\((?<body>.*)\)\s*(?:ON\s+\[PRIMARY\])?\s*$", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex CreateTableRegex();
[GeneratedRegex(@"CONSTRAINT\s+\[(?<name>[^\]]+)\]\s+PRIMARY\s+KEY(?:\s+(?:NON)?CLUSTERED)?\s*\((?<columns>[^)]*)\)", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex PrimaryKeyRegex();
[GeneratedRegex(@"^\s*\[(?<name>[^\]]+)\]\s+(?<type>[A-Za-z0-9]+(?:\s*\([^)]*\))?)(?<identity>\s+IDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\))?(?<null>\s+NOT\s+NULL|\s+NULL)?(?:\s+DEFAULT\s+(?<default>.+?))?\s*,?\s*$", RegexOptions.IgnoreCase)]
private static partial Regex ColumnRegex();
[GeneratedRegex(@"^ALTER\s+TABLE\s+(?<table>\S+)\s+ADD\s+CONSTRAINT\s+\[(?<name>[^\]]+)\]\s+(?<definition>(?:CHECK|UNIQUE|FOREIGN\s+KEY)\b.*)$", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex AddConstraintRegex();
[GeneratedRegex(@"^CREATE\s+(?<unique>UNIQUE\s+)?(?:(?:NON)?CLUSTERED\s+)?INDEX\s+\[(?<name>[^\]]+)\]\s+ON\s+(?<table>\S+)\s*\((?<columns>[^)]*)\)(?:\s+INCLUDE\s*\((?<include>[^)]*)\))?(?:\s+WHERE\s+(?<where>.+?))?\s*$", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex CreateIndexRegex();
[GeneratedRegex(@"\b(?:NON)?CLUSTERED\b\s*", RegexOptions.IgnoreCase)]
private static partial Regex ClusteringRegex();
[GeneratedRegex(@"(\])\s+(?:ASC|DESC)\b", RegexOptions.IgnoreCase)]
private static partial Regex SortOrderInListRegex();
[GeneratedRegex(@"\s+(?:ASC|DESC)$", RegexOptions.IgnoreCase)]
private static partial Regex SortOrderRegex();
[GeneratedRegex(@"\[dbo\]\.\[(?<name>[^\]]+)\]", RegexOptions.IgnoreCase)]
private static partial Regex DboIdentifierRegex();
[GeneratedRegex(@"\[(?<name>[^\]]+)\]")]
private static partial Regex BracketIdentifierRegex();
[GeneratedRegex(@"\bN'")]
private static partial Regex UnicodeLiteralRegex();
[GeneratedRegex(@"""(?<column>[A-Za-z0-9_]+)""\s*=\s*(?<value>[01])\b")]
private static partial Regex BitComparisonRegex();
[GeneratedRegex(@"\[(?<name>[^\]]+)\]\s+bit\b", RegexOptions.IgnoreCase)]
private static partial Regex BitColumnRegex();
[GeneratedRegex(@"^(?<name>[a-z]+)\((?<size>[0-9,]+)\)$")]
private static partial Regex SizedTypeRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhitespaceRegex();
}

View file

@ -76,7 +76,7 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
else if (defaultField.Value == PlatformConsts.DefaultValues.UserName)
value = CurrentUser.UserName;
else if (defaultField.Value == PlatformConsts.DefaultValues.Roles)
value = CurrentUser.Roles; //.JoinAsString("','");
value = CurrentUser.Roles;
else if (defaultField.Value == PlatformConsts.DefaultValues.Date)
value = Clock.Now.Date;
else if (defaultField.Value == PlatformConsts.DefaultValues.Now)
@ -117,8 +117,6 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
}
}
}
//value = field.Value;
//TODO: artirilabilir
break;
case FieldCustomValueTypeEnum.QueryParams:
if (queryParameters != null)

View file

@ -169,7 +169,6 @@ public class QueryManager : PlatformDomainService, IQueryManager
// oncelik Command alanindadir, dolu ise silme islemi buradaki sorguya yonlendirilir
if (!string.IsNullOrEmpty(command))
{
// sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType);
sql = command;
}
else
@ -256,78 +255,5 @@ public class QueryManager : PlatformDomainService, IQueryManager
_ => "1 = 0",
};
}
// private static string NormalizeCollectionParameterCommand(
// string command,
// Dictionary<string, object> parameters,
// DataSourceTypeEnum dataSourceType)
// {
// if (string.IsNullOrWhiteSpace(command) || parameters == null || parameters.Count == 0)
// {
// return command;
// }
// var sql = command;
// foreach (var parameter in parameters)
// {
// if (!IsCollectionParameter(parameter.Value))
// {
// continue;
// }
// var escapedParameterName = Regex.Escape(parameter.Key);
// sql = dataSourceType switch
// {
// DataSourceTypeEnum.Mssql => NormalizeMssqlCollectionParameter(sql, escapedParameterName, parameter.Key),
// DataSourceTypeEnum.Postgresql => NormalizePostgresqlCollectionParameter(sql, escapedParameterName, parameter.Key),
// _ => sql
// };
// }
// return sql;
// }
// private static bool IsCollectionParameter(object value)
// {
// return value is System.Collections.IEnumerable
// && value is not string
// && value is not byte[];
// }
// private static string NormalizeMssqlCollectionParameter(
// string sql,
// string escapedParameterName,
// string parameterName)
// {
// sql = Regex.Replace(
// sql,
// $@"IN\s*\(\s*@{escapedParameterName}\s*\)",
// $"IN @{parameterName}",
// RegexOptions.IgnoreCase);
// return Regex.Replace(
// sql,
// $@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
// match => $"{match.Groups["column"].Value} IN @{parameterName}",
// RegexOptions.IgnoreCase);
// }
// private static string NormalizePostgresqlCollectionParameter(
// string sql,
// string escapedParameterName,
// string parameterName)
// {
// sql = Regex.Replace(
// sql,
// $@"IN\s*\(\s*@{escapedParameterName}\s*\)",
// $"= ANY(@{parameterName})",
// RegexOptions.IgnoreCase);
// return Regex.Replace(
// sql,
// $@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
// match => $"{match.Groups["column"].Value} = ANY(@{parameterName})",
// RegexOptions.IgnoreCase);
// }
}

View file

@ -40,7 +40,12 @@ public class SpRepository : ISpRepository
await using var connection = CreateDbConnection(dataSourceType, connectionString);
await connection.OpenAsync();
await using var command = CreateDbCommand(connection, sp, dataSourceType, parameters);
// PostgreSQL'de prosedür `CALL`, fonksiyon `SELECT` ile çağrılır; tür adından bilinmez, katalogdan okunur.
var isPostgreSqlProcedure = dataSourceType == DataSourceTypeEnum.Postgresql
&& await IsPostgreSqlProcedureAsync(connection, PostgreSqlRoutineName(sp));
await using var command = CreateDbCommand(connection, sp, dataSourceType, parameters, isPostgreSqlProcedure);
return await ExecuteQueryAsync(command);
}
@ -55,7 +60,16 @@ public class SpRepository : ISpRepository
};
}
private DbCommand CreateDbCommand(DbConnection connection, string sp, DataSourceTypeEnum dataSourceType, Dictionary<string, object> parameters)
private static async Task<bool> IsPostgreSqlProcedureAsync(DbConnection connection, string name)
{
await using var command = connection.CreateCommand();
command.CommandText = "SELECT p.prokind FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' AND p.proname = @name LIMIT 1";
command.Parameters.Add(new NpgsqlParameter("@name", name));
return await command.ExecuteScalarAsync() is char kind && kind == 'p';
}
private DbCommand CreateDbCommand(DbConnection connection, string sp, DataSourceTypeEnum dataSourceType, Dictionary<string, object> parameters, bool isPostgreSqlProcedure)
{
var command = connection.CreateCommand();
command.CommandTimeout = 300; // 5 dakika timeout
@ -64,7 +78,7 @@ public class SpRepository : ISpRepository
command.CommandText = dataSourceType switch
{
DataSourceTypeEnum.Mssql => BuildSqlForMssql(sp, parameters),
DataSourceTypeEnum.Postgresql => BuildSqlForPostgresql(sp, parameters),
DataSourceTypeEnum.Postgresql => BuildSqlForPostgresql(sp, parameters, isPostgreSqlProcedure),
_ => throw new Exception("Unsupported database type")
};
@ -79,11 +93,28 @@ public class SpRepository : ISpRepository
: $"EXEC {sp}";
}
private string BuildSqlForPostgresql(string sp, Dictionary<string, object> parameters)
private static string BuildSqlForPostgresql(string sp, Dictionary<string, object> parameters, bool isProcedure)
{
return parameters != null && parameters.Count > 0
? $"SELECT * FROM public.{sp}({string.Join(", ", parameters.Keys.Select((k, i) => $"@p{i}"))})"
: $"SELECT * FROM public.{sp}()";
var arguments = parameters != null && parameters.Count > 0
? string.Join(", ", parameters.Keys.Select((k, i) => $"@p{i}"))
: string.Empty;
var routine = $"public.\"{PostgreSqlRoutineName(sp)}\"({arguments})";
return isProcedure ? $"CALL {routine}" : $"SELECT * FROM {routine}";
}
/// <summary>
/// Rutin adı SQL Server'daki gibi PascalCase yazılır (`Hr_S_TimesheetNightly`); PostgreSQL tırnaksız adı
/// küçük harfe indirdiği için ad tırnaklanarak kullanılır. `dbo.` öneki SQL Server şemasıdır, PG'de `public`.
/// </summary>
private static string PostgreSqlRoutineName(string sp)
{
var name = sp.Split('.').Last().Trim('[', ']', '"');
if (name.Length == 0 || !name.All(c => char.IsLetterOrDigit(c) || c == '_'))
throw new ArgumentException($"Invalid procedure name: {sp}", nameof(sp));
return name;
}
private void AddParameters(DbCommand command, DataSourceTypeEnum dataSourceType, Dictionary<string, object> parameters)

View file

@ -1225,12 +1225,18 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Order)), Prefix.DbSchema);
b.ConfigureByConvention();
var isActiveComputedColumnSql = _isPostgreSql
? "CASE WHEN \"LicenseEndTime\" IS NOT NULL AND CURRENT_DATE > \"LicenseEndTime\"::date THEN false ELSE true END"
: "CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)";
b.Property(o => o.IsActive)
.HasComputedColumnSql(isActiveComputedColumnSql, stored: false);
// PostgreSQL'de hesaplanan kolon yalnızca immutable ifade alır; CURRENT_DATE'e bağlı lisans
// kontrolü kolon olarak yazılamaz. Orada değer satırla saklanır, varsayılanı aktiftir.
if (_isPostgreSql)
{
b.Property(o => o.IsActive).HasDefaultValue(true);
}
else
{
b.Property(o => o.IsActive).HasComputedColumnSql(
"CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)",
stored: false);
}
b.Property(o => o.IsExisting).IsRequired().HasDefaultValue(false);
b.Property(o => o.OrganizationName).HasMaxLength(128);
b.Property(o => o.Founder).HasMaxLength(128);
@ -1841,6 +1847,11 @@ public class PlatformDbContext :
ConfigureProviderSpecificModel(builder);
}
/// <summary>
/// Model SQL Server sözdizimiyle yazılır (kolon tipi, index filtresi, varsayılan değer). PostgreSQL'de
/// aynı model bu sözdizimini sağlayıcının karşılığına çevirerek kurulur; SQL Server modeli ve
/// migration'ları bundan etkilenmez.
/// </summary>
private void ConfigureProviderSpecificModel(ModelBuilder builder)
{
if (!_isPostgreSql)
@ -1848,11 +1859,36 @@ public class PlatformDbContext :
return;
}
foreach (var index in builder.Model.GetEntityTypes().SelectMany(entityType => entityType.GetIndexes()))
foreach (var entityType in builder.Model.GetEntityTypes())
{
if (index.GetFilter() == "[IsDeleted] = 0")
foreach (var property in entityType.GetProperties())
{
index.SetFilter("\"IsDeleted\" = FALSE");
var columnType = property.GetColumnType();
if (columnType is not null)
{
property.SetColumnType(PostgreSqlModelSyntax.ColumnType(columnType));
}
var defaultValueSql = property.GetDefaultValueSql();
if (defaultValueSql is not null)
{
property.SetDefaultValueSql(PostgreSqlModelSyntax.DefaultValueSql(defaultValueSql));
}
}
foreach (var index in entityType.GetIndexes())
{
var filter = index.GetFilter();
if (filter is not null)
{
index.SetFilter(PostgreSqlModelSyntax.Expression(filter));
}
}
foreach (var checkConstraint in entityType.GetCheckConstraints().ToList())
{
entityType.RemoveCheckConstraint(checkConstraint.ModelName);
entityType.AddCheckConstraint(checkConstraint.ModelName, PostgreSqlModelSyntax.Expression(checkConstraint.Sql));
}
}
}

View file

@ -0,0 +1,70 @@
using System.Text.RegularExpressions;
namespace Sozsoft.Platform.EntityFrameworkCore;
/// <summary>
/// EF modelinde SQL Server sözdizimiyle yazılmış kolon tipi, varsayılan değer ve index filtresinin
/// PostgreSQL karşılığı. Model tek yerde yazılır; PostgreSQL'de <see cref="PlatformDbContext"/> bu
/// çeviriyle kurulur.
/// </summary>
internal static partial class PostgreSqlModelSyntax
{
public static string ColumnType(string sqlServerType)
{
var type = sqlServerType.Trim().ToLowerInvariant();
if (type is "nvarchar(max)" or "varchar(max)" or "ntext" or "text")
return "text";
var sized = SizedStringRegex().Match(type);
if (sized.Success)
return $"character varying({sized.Groups["size"].Value})";
return type switch
{
"uniqueidentifier" => "uuid",
"datetime2" or "datetime" or "smalldatetime" => "timestamp without time zone",
"datetimeoffset" => "timestamp with time zone",
"bit" => "boolean",
"tinyint" => "smallint",
"float" => "double precision",
"real" => "real",
"varbinary(max)" or "image" => "bytea",
_ => sqlServerType,
};
}
public static string DefaultValueSql(string sqlServerSql)
{
return sqlServerSql.Trim().ToUpperInvariant() switch
{
"NEWID()" or "NEWSEQUENTIALID()" => "gen_random_uuid()",
"SYSDATETIME()" or "GETDATE()" => "LOCALTIMESTAMP",
"SYSUTCDATETIME()" or "GETUTCDATE()" => "(NOW() AT TIME ZONE 'UTC')",
_ => sqlServerSql,
};
}
/// <summary>
/// Index filtresi ya da check constraint ifadesi: köşeli parantezli kolon adını tırnaklı ada, bit
/// karşılaştırmasını boolean karşılaştırmaya çevirir
/// (<c>[IsDeleted] = 0</c> → <c>"IsDeleted" = FALSE</c>).
/// </summary>
public static string Expression(string sqlServerExpression)
{
var expression = BracketIdentifierRegex().Replace(sqlServerExpression, "\"$1\"");
return BitComparisonRegex().Replace(
expression,
match => $"{match.Groups["column"].Value} = {(match.Groups["value"].Value == "1" ? "TRUE" : "FALSE")}");
}
[GeneratedRegex(@"^n?varchar\((?<size>\d+)\)$")]
private static partial Regex SizedStringRegex();
[GeneratedRegex(@"\[([^\]]+)\]")]
private static partial Regex BracketIdentifierRegex();
[GeneratedRegex(@"(?<column>""Is[A-Za-z]+"")\s*=\s*(?<value>[01])\b")]
private static partial Regex BitComparisonRegex();
}

View file

@ -1,7 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
@ -38,6 +39,23 @@ public static class SetupMigrationRunner
/// <summary>Alt surece seed klasorunu bildiren ortam degiskeni (App:SeedsPath karsiligi).</summary>
private const string SeedsPathEnvironmentVariable = "App__SeedsPath";
private const string BuildStampFileName = "setup-migrator.stamp";
/// <summary>
/// Kaynak projeden çalışırken derleme çıktısını eskiten dosyalar. Seed klasörü çalışma anında
/// okunduğu için burada değildir.
/// </summary>
private static readonly HashSet<string> SourceExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".cs", ".csproj", ".props", ".targets", ".json", ".resx"
};
/// <summary>Derleme çıktısı ya da çalışma anında yazılan klasörler; tazelik kararına girmez.</summary>
private static readonly HashSet<string> IgnoredSourceDirectories = new(StringComparer.OrdinalIgnoreCase)
{
"bin", "obj", "Logs", "artifacts", "wwwroot", "node_modules", ".vs", ".git"
};
/// <param name="processCancellationToken">
/// Sürecin yaşam süresini kontrol eden token. Bu <b>istemci isteğinin</b> token'ı DEĞİL,
/// uygulamanın <c>ApplicationStopping</c> token'ı olmalıdır. Aksi halde kullanıcı sekmeyi
@ -86,8 +104,6 @@ public static class SetupMigrationRunner
return MigrationOutcome.MigratorNotFound;
}
await stream.InfoAsync($"Running: {command!.FileName} {command.Arguments}");
var timeout = TimeSpan.FromMinutes(
configuration.GetValue<int?>("Setup:MigratorTimeoutMinutes") ?? DefaultTimeoutMinutes);
@ -95,39 +111,229 @@ public static class SetupMigrationRunner
timeoutCts.CancelAfter(timeout);
var token = timeoutCts.Token;
Process? process = null;
try
{
process = new Process
if (command!.ProjectPath is not null)
{
StartInfo = new ProcessStartInfo
var migratorDll = await PrepareSourceMigratorAsync(command.ProjectPath, stream, token);
if (migratorDll is null)
{
FileName = command.FileName,
Arguments = command.Arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = command.WorkingDirectory,
await stream.DoneAsync("Failed.");
return MigrationOutcome.Failed;
}
command = command with { Arguments = $"\"{migratorDll}\" {command.Arguments}" };
}
await stream.InfoAsync($"Running: {command.FileName} {command.Arguments}");
var processEnvironment = new Dictionary<string, string>
{
// Seed klasoru artik migrator publish ciktisinin icinde degil, mount edilen ortak
// klasorde. Alt surec API'nin cozdugu yolu birebir kullansin ki uygulamadan
// tetiklenen migrate, File Manager'da gorunen dosyalari islesin.
[SeedsPathEnvironmentVariable] = SeedPathResolver.GetRootPath(configuration),
};
// Seed klasoru artik migrator publish ciktisinin icinde degil, mount edilen ortak
// klasorde. Alt surec API'nin cozdugu yolu birebir kullansin ki uygulamadan
// tetiklenen migrate, File Manager'da gorunen dosyalari islesin.
process.StartInfo.Environment[SeedsPathEnvironmentVariable] =
SeedPathResolver.GetRootPath(configuration);
// DLL doğrudan çalıştığında launchSettings okunmaz; kaynak projeden çalışma onun ortamını taşır.
if (command.ProjectPath is not null)
processEnvironment["DOTNET_ENVIRONMENT"] = "Development";
if (environment is not null)
{
foreach (var (name, value) in environment)
{
process.StartInfo.Environment[name] = value;
processEnvironment[name] = value;
}
}
process.Start();
var exitCode = await RunProcessAsync(command, processEnvironment, stream, token);
// Şema değişmiş olabilir — bir sonraki durum sorgusu taze veri görsün.
DatabaseReadinessChecker.Invalidate();
if (exitCode == 0)
{
return MigrationOutcome.Succeeded;
}
await stream.ErrorAsync($"Migration failed. Exit code: {exitCode}");
await stream.DoneAsync("Failed.");
return MigrationOutcome.Failed;
}
catch (OperationCanceledException)
{
DatabaseReadinessChecker.Invalidate();
var timedOut = !processCancellationToken.IsCancellationRequested;
await stream.ErrorAsync(timedOut
? $"Migration timed out after {timeout.TotalMinutes:0} minutes and was terminated."
: "Migration was canceled because the application is shutting down.");
await stream.DoneAsync("Failed.");
return timedOut ? MigrationOutcome.TimedOut : MigrationOutcome.Failed;
}
catch (Exception ex)
{
DatabaseReadinessChecker.Invalidate();
await stream.ErrorAsync($"Migration failed: {ex.Message}");
await stream.DoneAsync("Failed.");
return MigrationOutcome.Failed;
}
}
/// <summary>
/// Kaynak projeden çalışırken migrator DLL'ini hazırlar. <c>dotnet run</c> her seferinde
/// çıktısını gizleyerek derler; değişiklik yokken bile bu ~25 sn sessiz bekleme demektir.
/// DLL kaynaklardan yeniyse derleme atlanır, değilse derleme çıktısı kurulum loguna akar.
/// </summary>
private static async Task<string?> PrepareSourceMigratorAsync(
string projectPath,
SetupEventStream stream,
CancellationToken token)
{
// MSBuild değişmeyen çıktıyı yeniden yazmaz; derlemeye girmeyen bir dosya değiştiğinde DLL'in
// tarihi eski kalır ve her çalıştırma yeniden derlerdi. Son başarılı derlemenin anı damgadadır.
var stampPath = Path.Combine(projectPath, "obj", BuildStampFileName);
var migratorDll = FindBuiltMigrator(projectPath);
if (migratorDll is not null && IsUpToDate(projectPath, LastBuildTimeUtc(migratorDll, stampPath)))
{
await stream.InfoAsync("DbMigrator is up to date, build skipped.");
return migratorDll;
}
await stream.InfoAsync("Building DbMigrator...");
var exitCode = await RunProcessAsync(
new MigratorCommand("dotnet", $"build \"{projectPath}\" -nologo -v:m", projectPath),
null,
stream,
token);
if (exitCode != 0)
{
await stream.ErrorAsync($"DbMigrator build failed. Exit code: {exitCode}");
return null;
}
migratorDll = FindBuiltMigrator(projectPath);
if (migratorDll is null)
{
await stream.ErrorAsync($"DbMigrator build output not found under {projectPath}.");
return null;
}
await File.WriteAllTextAsync(stampPath, string.Empty, token);
return migratorDll;
}
private static DateTime LastBuildTimeUtc(string migratorDll, string stampPath)
{
var builtAt = File.GetLastWriteTimeUtc(migratorDll);
return File.Exists(stampPath) && File.GetLastWriteTimeUtc(stampPath) > builtAt
? File.GetLastWriteTimeUtc(stampPath)
: builtAt;
}
/// <summary>
/// Debug derleme çıktısındaki migrator DLL'i; birden çok hedef çerçeve klasörü varsa en yenisi
/// (eski çerçevenin kalıntısı seçilmez).
/// </summary>
private static string? FindBuiltMigrator(string projectPath)
{
var outputRoot = Path.Combine(projectPath, "bin", "Debug");
if (!Directory.Exists(outputRoot))
{
return null;
}
var dllName = Path.GetFileName(projectPath) + ".dll";
return Directory.GetDirectories(outputRoot)
.Select(directory => Path.Combine(directory, dllName))
.Where(File.Exists)
.OrderByDescending(File.GetLastWriteTimeUtc)
.FirstOrDefault();
}
/// <summary>
/// Migrator'ün bağlı olduğu kaynaklar (api/src, api/modules ve kök props dosyaları) DLL'den
/// sonra değişmediyse true. Yanlış "değişti" kararı yalnızca gereksiz bir derlemeye mal olur.
/// </summary>
private static bool IsUpToDate(string projectPath, DateTime builtAtUtc)
{
var apiRoot = Directory.GetParent(projectPath)?.Parent;
if (apiRoot is null)
{
return false;
}
return !EnumerateSourceFiles(apiRoot).Any(file => File.GetLastWriteTimeUtc(file) > builtAtUtc);
}
private static IEnumerable<string> EnumerateSourceFiles(DirectoryInfo root)
{
var pending = new Stack<DirectoryInfo>();
pending.Push(root);
while (pending.Count > 0)
{
var directory = pending.Pop();
foreach (var file in directory.EnumerateFiles())
{
if (SourceExtensions.Contains(file.Extension))
{
yield return file.FullName;
}
}
foreach (var child in directory.EnumerateDirectories())
{
if (!IgnoredSourceDirectories.Contains(child.Name))
{
pending.Push(child);
}
}
}
}
/// <summary>Süreci çalıştırır, stdout/stderr satırlarını kurulum loguna aktarır ve çıkış kodunu döndürür.</summary>
private static async Task<int> RunProcessAsync(
MigratorCommand command,
IReadOnlyDictionary<string, string>? environment,
SetupEventStream stream,
CancellationToken token)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command.FileName,
Arguments = command.Arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = command.WorkingDirectory,
}
};
if (environment is not null)
{
foreach (var (name, value) in environment)
{
process.StartInfo.Environment[name] = value;
}
}
process.Start();
try
{
async Task PumpAsync(StreamReader reader, string level)
{
try
@ -149,50 +355,20 @@ public static class SetupMigrationRunner
await process.WaitForExitAsync(token);
// Şema değişmiş olabilir — bir sonraki durum sorgusu taze veri görsün.
DatabaseReadinessChecker.Invalidate();
if (process.ExitCode == 0)
{
return MigrationOutcome.Succeeded;
}
await stream.ErrorAsync($"Migration failed. Exit code: {process.ExitCode}");
await stream.DoneAsync("Failed.");
return MigrationOutcome.Failed;
return process.ExitCode;
}
catch (OperationCanceledException)
catch
{
KillQuietly(process);
DatabaseReadinessChecker.Invalidate();
var timedOut = !processCancellationToken.IsCancellationRequested;
await stream.ErrorAsync(timedOut
? $"Migration timed out after {timeout.TotalMinutes:0} minutes and was terminated."
: "Migration was canceled because the application is shutting down.");
await stream.DoneAsync("Failed.");
return timedOut ? MigrationOutcome.TimedOut : MigrationOutcome.Failed;
}
catch (Exception ex)
{
KillQuietly(process);
DatabaseReadinessChecker.Invalidate();
await stream.ErrorAsync($"Migration failed: {ex.Message}");
await stream.DoneAsync("Failed.");
return MigrationOutcome.Failed;
}
finally
{
process?.Dispose();
throw;
}
}
private static void KillQuietly(Process? process)
private static void KillQuietly(Process process)
{
try
{
if (process is { HasExited: false })
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
@ -227,7 +403,6 @@ public static class SetupMigrationRunner
if (migratorPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(migratorPath))
{
// Doğrudan DLL yolu — "--" separator yok, argümanlar doğrudan geçer.
command = new MigratorCommand(
"dotnet",
$"\"{migratorPath}\" {extraArgs}",
@ -246,8 +421,8 @@ public static class SetupMigrationRunner
command = dllFiles.Length > 0
// Publish çıktısı — SDK gerekmez.
? new MigratorCommand("dotnet", $"\"{dllFiles[0]}\" {extraArgs}", migratorPath)
// Kaynak proje klasörü — geliştirme ortamı, "--" separator gerekli.
: new MigratorCommand("dotnet", $"run --project \"{migratorPath}\" -- {extraArgs}", migratorPath);
// Kaynak proje klasörü — DLL çalıştırmadan önce PrepareSourceMigratorAsync ile hazırlanır.
: new MigratorCommand("dotnet", extraArgs, migratorPath, migratorPath);
return true;
}
@ -256,4 +431,5 @@ public static class SetupMigrationRunner
}
}
internal sealed record MigratorCommand(string FileName, string Arguments, string WorkingDirectory);
/// <param name="ProjectPath">Dolu ise komut kaynak projeden çalışır; <c>Arguments</c> yalnızca migrator argümanlarıdır.</param>
internal sealed record MigratorCommand(string FileName, string Arguments, string WorkingDirectory, string? ProjectPath = null);

View file

@ -383,7 +383,13 @@ public class DynamicFormReport : XtraReport
CanGrow = true
};
if (TryGetFormattedValue(field, row, out var text))
var markup = GetColorMarkup(field, row);
if (!string.IsNullOrEmpty(markup))
{
label.AllowMarkupText = true;
label.Text = markup;
}
else if (TryGetFormattedValue(field, row, out var text))
{
label.Text = text;
label.Font = new DXFont(
@ -402,6 +408,28 @@ public class DynamicFormReport : XtraReport
return label;
}
/// <summary>
/// Rengi olan lookup için renkli rozetler, renk kutusu için renk örneği; diğer alanlarda <c>null</c>.
/// </summary>
private static string GetColorMarkup(FormReportField field, IDictionary<string, object> row)
{
if (DynamicReportSqlBuilder.HasLookupColor(field.Lookup))
{
return DynamicReportImageHelper.TryGetRowValue(
row,
DynamicReportSqlBuilder.GetLookupColorBindingName(field.Field),
out var tags)
? DynamicReportColorHelper.ToTagsMarkup(tags)
: null;
}
return field.Lookup == null &&
field.EditorType == PlatformConsts.EditorTypes.dxColorBox &&
DynamicReportImageHelper.TryGetRowValue(row, field.BindingName, out var color)
? DynamicReportColorHelper.ToSwatchMarkup(color)
: null;
}
private static XRRichText CreateHtmlValueControl(
FormReportField field,
IDictionary<string, object> row,

View file

@ -0,0 +1,173 @@
using System;
using System.Globalization;
using System.Linq;
using DevExpress.Data.Filtering;
namespace Sozsoft.Reports.PredefinedReports;
/// <summary>
/// Renk kutusu (<c>dxColorBox</c>) kolonlarını ve rengi olan lookup değerlerini DevExpress markup
/// metniyle çizer. Hücrenin <c>AllowMarkupText</c> özelliği açık olmalıdır.
/// </summary>
internal static class DynamicReportColorHelper
{
// Ayraçlar SQL metnine yazılır ve rapor tanımı XML olarak saklanır; XML 1.0 kontrol
// karakterlerini (0x1F/0x1E) kabul etmediği için onların görünür karşılıkları kullanılır.
/// <summary>Lookup metni ile rengi arasındaki ayraç (SQL'de üretilir).</summary>
public const string PairSeparator = "\u241F";
/// <summary>Çok değerli lookup'ta öğeler arasındaki ayraç (SQL'de üretilir).</summary>
public const string ItemSeparator = "\u241E";
private const string SwatchFunctionName = "SozReportColorSwatch";
private const string TagsFunctionName = "SozReportColorTags";
private const string Space = " ";
static DynamicReportColorHelper()
{
Register(new ColorSwatchFunction());
Register(new ColorTagsFunction());
}
public static string CreateSwatchExpression(string valueExpression)
{
return $"{SwatchFunctionName}({valueExpression})";
}
public static string CreateTagsExpression(string valueExpression)
{
return $"{TagsFunctionName}({valueExpression})";
}
/// <summary>Renk değerinin yanında küçük bir renk kutusu.</summary>
public static string ToSwatchMarkup(object value)
{
var text = ToText(value);
if (!TryParseColor(text, out var color))
{
return text;
}
return $"<backcolor={ToHex(color)}>{Space}{Space}{Space}{Space}{Space}</backcolor>{Space}{text}";
}
/// <summary>"Metin␟Renk␞Metin␟Renk" çiftlerini renkli rozetlere çevirir.</summary>
public static string ToTagsMarkup(object value)
{
var text = ToText(value);
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
return string.Join(
" ",
text.Split(ItemSeparator, StringSplitOptions.RemoveEmptyEntries)
.Select(item =>
{
var parts = item.Split(PairSeparator);
var label = parts[0];
if (parts.Length < 2 || !TryParseColor(parts[1], out var color))
{
return label;
}
return $"<backcolor={ToHex(color)}><color={ToHex(ReadableTextColor(color))}>{Space}{label}{Space}</color></backcolor>";
}));
}
private static bool TryParseColor(string value, out System.Drawing.Color color)
{
color = System.Drawing.Color.Empty;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
try
{
color = System.Drawing.ColorTranslator.FromHtml(value.Trim());
return !color.IsEmpty;
}
catch (Exception ex) when (ex is ArgumentException or FormatException)
{
return false;
}
}
// Açık zeminde beyaz yazı okunmadığı için istemcideki rozetle aynı eşik kullanılır.
private static System.Drawing.Color ReadableTextColor(System.Drawing.Color background)
{
var brightness = (background.R * 299 + background.G * 587 + background.B * 114) / 1000;
return brightness > 150
? System.Drawing.Color.FromArgb(17, 24, 39)
: System.Drawing.Color.White;
}
private static string ToHex(System.Drawing.Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
private static string ToText(object value)
{
return value == null || value == DBNull.Value
? string.Empty
: Convert.ToString(value, CultureInfo.InvariantCulture)?.Trim() ?? string.Empty;
}
private static void Register(ICustomFunctionOperator function)
{
if (CriteriaOperator.GetCustomFunction(function.Name) == null)
{
CriteriaOperator.RegisterCustomFunction(function);
}
}
private abstract class ColorFunctionBase : ICustomFunctionOperatorBrowsable
{
public abstract string Name { get; }
public int MinOperandCount => 1;
public int MaxOperandCount => 1;
public FunctionCategory Category => FunctionCategory.Text;
public string Description => "Returns report markup text for a color value.";
public abstract object Evaluate(params object[] operands);
public Type ResultType(params Type[] operands)
{
return typeof(string);
}
public bool IsValidOperandCount(int count)
{
return count == 1;
}
public bool IsValidOperandType(int operandIndex, int operandCount, Type type)
{
return true;
}
}
private sealed class ColorSwatchFunction : ColorFunctionBase
{
public override string Name => SwatchFunctionName;
public override object Evaluate(params object[] operands)
{
return ToSwatchMarkup(operands.ElementAtOrDefault(0));
}
}
private sealed class ColorTagsFunction : ColorFunctionBase
{
public override string Name => TagsFunctionName;
public override object Evaluate(params object[] operands)
{
return ToTagsMarkup(operands.ElementAtOrDefault(0));
}
}
}

View file

@ -6,6 +6,7 @@ using DevExpress.Drawing;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform;
using Sozsoft.Platform.Localization;
namespace Sozsoft.Reports.PredefinedReports;
@ -115,15 +116,18 @@ internal static class DynamicReportLayoutHelper
$"[{column.BindingName}]",
column.Field,
column.EditorType);
var markupExpression = CreateColorMarkupExpression(column);
cell.AllowMarkupText = markupExpression != null;
var displayExpression = markupExpression ?? valueExpression;
var textExpression = hasPrefix
? $"Concat([{prefixColumnName}], {valueExpression})"
: valueExpression;
? $"Concat([{prefixColumnName}], {displayExpression})"
: displayExpression;
cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", textExpression));
cell.ExpressionBindings.Add(new ExpressionBinding(
"BeforePrint",
"Font.Name",
DynamicReportImageHelper.CreateFontFamilyExpression(textExpression)));
DynamicReportImageHelper.CreateFontFamilyExpression(valueExpression)));
if (!string.IsNullOrWhiteSpace(boldColumnName))
{
@ -133,6 +137,23 @@ internal static class DynamicReportLayoutHelper
return cell;
}
/// <summary>
/// Renkli çizilen kolonun markup ifadesi: rengi olan lookup rozet, renk kutusu renk örneği
/// olarak çizilir. Diğer kolonlarda <c>null</c>.
/// </summary>
public static string CreateColorMarkupExpression(DynamicReportColumn column)
{
if (DynamicReportSqlBuilder.HasLookupColor(column.Lookup))
{
return DynamicReportColorHelper.CreateTagsExpression(
$"[{DynamicReportSqlBuilder.GetLookupColorBindingName(column.Field)}]");
}
return column.Lookup == null && column.EditorType == PlatformConsts.EditorTypes.dxColorBox
? DynamicReportColorHelper.CreateSwatchExpression($"[{column.BindingName}]")
: null;
}
public static XRTableCell CreateHtmlDetailCell(DynamicReportColumn column, float width, float height)
{
var cell = new XRTableCell

View file

@ -16,6 +16,9 @@ internal static class DynamicReportSqlBuilder
private static readonly Regex LookupParameterRegex = new(
@"@\s*param\d+",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex LookupColorAliasRegex = new(
@"\bAS\s+(?:(?<quoted>\[Color\]|""Color"")|Color\b)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
internal interface IReportLookupColumn
{
@ -31,6 +34,43 @@ internal static class DynamicReportSqlBuilder
return $"{field.FieldName}__LookupText";
}
/// <summary>
/// Renk taşıyan lookup'ın görünen metin + renk çiftleri. Metin kolonu düz kalır (gruplama,
/// kriter metni ve kayıt metinleri onu okur); renkli çizim bu kolondan yapılır.
/// </summary>
internal static string GetLookupColorBindingName(ListFormField field)
{
return $"{field.FieldName}__LookupColor";
}
/// <summary>
/// Lookup sorgusu rengi <c>AS [Color]</c> takma adıyla döndürüyorsa kolon adını verir. Sorgu
/// rapor kurulurken çalıştırılıp kolonları okunmaz; sözleşme takma addır.
/// </summary>
internal static bool HasLookupColor(LookupDto lookup)
{
return GetLookupColorField(lookup, DataSourceTypeEnum.Mssql) != null;
}
internal static string GetLookupColorField(LookupDto lookup, DataSourceTypeEnum dataSourceType)
{
if (lookup == null || !IsReportQueryableLookup(lookup))
{
return null;
}
var match = LookupColorAliasRegex.Match(lookup.LookupQuery);
if (!match.Success)
{
return null;
}
// Tırnaksız takma ad PostgreSQL'de küçük harfe çevrilir.
return match.Groups["quoted"].Success || dataSourceType == DataSourceTypeEnum.Mssql
? "Color"
: "color";
}
internal static bool IsReportQueryableLookup(LookupDto lookup)
{
return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query &&
@ -142,11 +182,39 @@ internal static class DynamicReportSqlBuilder
var sourceField = GetSourceField(column.Field.FieldName);
var resultAlias = QuoteSqlIdentifier(column.BindingName);
var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql
? $"SELECT TOP 1 {lookupAlias}.{displayExpr} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}"
: $"SELECT {lookupAlias}.{displayExpr} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1";
string CreateScalarQuery(string selectExpression) => dataSourceType == DataSourceTypeEnum.Mssql
? $"SELECT TOP 1 {selectExpression} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}"
: $"SELECT {selectExpression} FROM ({lookupSelectQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1";
return new LookupSelectPart($"({scalarQuery}) AS {resultAlias}", commonTableExpression);
var selectSql = $"({CreateScalarQuery($"{lookupAlias}.{displayExpr}")}) AS {resultAlias}";
var colorPair = CreateColorPairExpression(column, lookupAlias, displayExpr, dataSourceType);
if (colorPair != null)
{
selectSql += $", ({CreateScalarQuery(colorPair)}) AS {QuoteSqlIdentifier(GetLookupColorBindingName(column.Field))}";
}
return new LookupSelectPart(selectSql, commonTableExpression);
}
/// <summary>
/// Görünen metin ile rengi <see cref="DynamicReportColorHelper"/>'ın çözdüğü ayraçla birleştirir.
/// </summary>
private static string CreateColorPairExpression(
IReportLookupColumn column,
string lookupAlias,
string displayExpr,
DataSourceTypeEnum dataSourceType)
{
var colorField = GetLookupColorField(column.Lookup, dataSourceType);
if (colorField == null)
{
return null;
}
var colorExpr = $"{lookupAlias}.{QuoteSqlIdentifier(colorField)}";
return dataSourceType == DataSourceTypeEnum.Mssql
? $"CAST({lookupAlias}.{displayExpr} AS NVARCHAR(MAX)) + N'{DynamicReportColorHelper.PairSeparator}' + COALESCE(CAST({colorExpr} AS NVARCHAR(64)), N'')"
: $"CAST({lookupAlias}.{displayExpr} AS TEXT) || '{DynamicReportColorHelper.PairSeparator}' || COALESCE(CAST({colorExpr} AS TEXT), '')";
}
private static LookupSelectPart CreateStaticMultiLookupSelectPart(
@ -190,21 +258,31 @@ internal static class DynamicReportSqlBuilder
var sourceField = GetSourceField(column.Field.FieldName);
var resultAlias = QuoteSqlIdentifier(column.BindingName);
var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql
string CreateAggregateQuery(string itemExpression, string separator) => dataSourceType == DataSourceTypeEnum.Mssql
? $"""
SELECT STRING_AGG(CAST({lookupAlias}.{displayExpr} AS NVARCHAR(MAX)), N', ')
SELECT STRING_AGG({itemExpression}, N'{separator}')
FROM ({lookupSelectQuery}) AS {lookupAlias}
INNER JOIN STRING_SPLIT(CAST({sourceField} AS NVARCHAR(MAX)), N'{PlatformConsts.MultiValueDelimiter}') AS SplitValues
ON CAST({lookupAlias}.{valueExpr} AS NVARCHAR(4000)) = LTRIM(RTRIM(SplitValues.value))
""".ReplaceLineEndings(" ").Trim()
: $"""
SELECT string_agg(CAST({lookupAlias}.{displayExpr} AS TEXT), ', ')
SELECT string_agg({itemExpression}, '{separator}')
FROM ({lookupSelectQuery}) AS {lookupAlias}
INNER JOIN unnest(string_to_array(CAST({sourceField} AS TEXT), '{PlatformConsts.MultiValueDelimiter}')) AS SplitValues(value)
ON CAST({lookupAlias}.{valueExpr} AS TEXT) = btrim(SplitValues.value)
""".ReplaceLineEndings(" ").Trim();
return new LookupSelectPart($"({scalarQuery}) AS {resultAlias}", commonTableExpression);
var displayItem = dataSourceType == DataSourceTypeEnum.Mssql
? $"CAST({lookupAlias}.{displayExpr} AS NVARCHAR(MAX))"
: $"CAST({lookupAlias}.{displayExpr} AS TEXT)";
var selectSql = $"({CreateAggregateQuery(displayItem, ", ")}) AS {resultAlias}";
var colorPair = CreateColorPairExpression(column, lookupAlias, displayExpr, dataSourceType);
if (colorPair != null)
{
selectSql += $", ({CreateAggregateQuery(colorPair, DynamicReportColorHelper.ItemSeparator)}) AS {QuoteSqlIdentifier(GetLookupColorBindingName(column.Field))}";
}
return new LookupSelectPart(selectSql, commonTableExpression);
}
private static (string CommonTableExpression, string SelectQuery) SplitCommonTableExpression(string query)

View file

@ -114,6 +114,7 @@ These sections encode expectations the user will **not** repeat in every prompt.
| Bir modülün nasıl çalıştığı, analizi ve kararları | `configs/seeds/docs/modules/{Modül}.md` (dizin: `README.md` §10.5) |
| Yetki kodları | `PlatformConsts.AppCodes` (backend) + `constants/permission.constant.ts` (UI) |
| Seed yolları | `SeedPathResolver` |
| SQL Server → PostgreSQL çeviri kuralları | `.github/instructions/lowcode.instructions.md` §8.3.1 |
Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → bu dosya → `README.md`.
@ -196,6 +197,20 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
menüde nerede / hangi ekranın sekmesi olduğu, ana alanlar, otomatik hesap ve geçişler, ilişkili
ekranlar (`RichText` biçimi). MCP `deploy_wizard` 3 satır / 150 karakterin altını reddeder.
Ayrıntı: `lowcode.instructions.md` §4.4.2.
16. **İki veritabanı sağlayıcısı** → uygulama SQL Server ile olduğu gibi PostgreSQL ile de çalışır
(`SettingsConsts.DefaultDatabaseProvider`). Veritabanına dokunan her değişiklik ikisi için birlikte
düşünülür ve yazılır; kullanıcı bunu her istekte hatırlatmaz:
- `configs/seeds/sql/{object,execute,host/…}/X.sql` yazılan/değişen her dosyanın
`configs/seeds/postgres/…/X.sql` eşi aynı işte yazılır/güncellenir (aynı ad, aynı parametre sırası,
şema PG'de `public`). Biri olmadan diğeri commit edilmez; "PG sonra" yoktur.
- EF modeli SQL Server sözdizimiyle yazılır (kolon tipi, index filtresi, check, varsayılan); PG
karşılığı `PostgreSqlModelSyntax` üzerinden `PlatformDbContext.ConfigureProviderSpecificModel`'de
üretilir. Çeviriye girmeyen yeni bir sözdizimi eklenirse çeviri de eklenir.
- C#'ta üretilen SQL (Dapper, `QueryManager`, seeder komutları, ekran `*Command`/`LookupQuery`
metinleri) ya iki sağlayıcıda da çalışan tırnaklı/ANSI biçimde yazılır ya da `DataSourceType`'a göre
dallanır.
- SQL değişikliği doğrulanırken PG eşi de bir PostgreSQL veritabanında çalıştırılır (iki kez —
idempotent). Ayrıntı ve dönüşüm tablosu: `lowcode.instructions.md` §8.3.1.
### Cross-cutting: touching one thing means touching these too
@ -334,7 +349,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Write`'a ek olarak `App.SqlQueryManager`
ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir
(`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir.
(`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir. Tasarımcı
(`SaveTableScriptAsync`) ve `create_table` T-SQL dosyanın PostgreSQL eşini `PostgreSqlTableScript` ile
üretip `postgres/object/` altına yazar; script biçimine yeni bir deyim eklenirse çevirisi de eklenir,
yoksa PG eşi yazılmaz (log uyarısı). Düşürülen nesnenin dosyası iki klasörden de silinir.
- **Çalışan (persona) ekranları ve profil** → çalışanın/yöneticinin kendi listeleri İK menüsüne
konmaz: menüsüz `App.Hr.My*`/`Team*` wizard + Intranet panosu widget'ı. Profil sayfasının İzinlerim /
Puantajım / Performansım sekmeleri persona ekranlarını salt okunur gösteren çekirdek bileşenlerdir

View file

@ -69780,20 +69780,6 @@
"Key": "App.Mcp.Error.TableAlreadyExists",
"Value": "\"{0}\" tablosu zaten var. Var olan tabloları SQL Query Manager tablo tasarımcısından değiştirin."
},
{
"Id": "a8300ba4-adcf-5a61-87a9-ea5a5821918f",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Mcp.Error.TableProviderNotSupported",
"Value": "Creating tables over MCP is supported only on SQL Server data sources (current: {0})."
},
{
"Id": "d95bd46b-1cb1-5ed6-b26a-f5b2c633c41e",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Mcp.Error.TableProviderNotSupported",
"Value": "MCP üzerinden tablo oluşturma yalnızca SQL Server veri kaynaklarında desteklenir (mevcut: {0})."
},
{
"Id": "d88ec341-a3cd-505f-b14a-2c6ce3f82de9",
"CultureName": "en",

View file

@ -1,170 +1,88 @@
CREATE OR REPLACE FUNCTION dbo."Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns"
-- SQL Server surumu kayit sonrasinda eklenen satiri da secer; PostgreSQL proseduru satir
-- donduremez ve FinalSeeder yalnizca CALL ile cagirir, bu yuzden o son SELECT yoktur.
CREATE OR REPLACE PROCEDURE public."Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns"
(
p_tenant_name VARCHAR(128) DEFAULT 'Sozsoft',
p_definition_culture_name VARCHAR(10) DEFAULT 'en',
p_ui_culture_name VARCHAR(10) DEFAULT 'tr',
p_list_form_code VARCHAR(64) DEFAULT NULL,
p_only_base_definitions BOOLEAN DEFAULT TRUE,
p_resource_name VARCHAR(64) DEFAULT NULL,
p_max_forms INTEGER DEFAULT 500,
p_deactivate_previous BOOLEAN DEFAULT TRUE,
p_note VARCHAR(4000) DEFAULT NULL
)
RETURNS TABLE
(
"TenantName" VARCHAR,
"DictionaryType" VARCHAR,
"DefinitionCultureName" VARCHAR,
"UiCultureName" VARCHAR,
"ListFormCode" VARCHAR,
"OnlyBaseDefinitions" BOOLEAN,
"ResourceName" VARCHAR,
"MaxForms" INTEGER,
"CacheKey" TEXT,
"FormCount" INTEGER,
"FieldCount" INTEGER,
"JsonLength" INTEGER,
"IsActive" BOOLEAN,
"RefreshedAt" TIMESTAMP,
"SourceProcedure" VARCHAR,
"Note" VARCHAR
p_TenantName varchar(128) DEFAULT 'Sozsoft',
p_DefinitionCultureName varchar(10) DEFAULT 'en',
p_UiCultureName varchar(10) DEFAULT 'tr',
p_ListFormCode varchar(64) DEFAULT NULL,
p_OnlyBaseDefinitions boolean DEFAULT TRUE,
p_ResourceName varchar(64) DEFAULT NULL,
p_MaxForms integer DEFAULT 500,
p_DeactivatePrevious boolean DEFAULT TRUE,
p_Note varchar(4000) DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_json_data JSONB;
v_cache_key TEXT;
v_form_count INTEGER := 0;
v_field_count INTEGER := 0;
v_json_length INTEGER := 0;
v_JsonData text;
v_Json jsonb;
v_CacheKey varchar(512);
v_FormCount integer := 0;
v_FieldCount integer := 0;
v_JsonLength integer;
BEGIN
/*
SQL Server:
EXEC dbo.Sas_H_ListFormFieldsDictionaryJson
PostgreSQL tarafında bunun JSON / JSONB döndüren bir FUNCTION
olduğu varsayılmıştır.
*/
SELECT dbo."Sas_H_ListFormFieldsDictionaryJson"
(
p_definition_culture_name,
p_ui_culture_name,
p_list_form_code,
p_only_base_definitions,
p_resource_name
)
INTO v_json_data;
/*
JSON boş kontrolü
*/
IF v_json_data IS NULL THEN
RAISE EXCEPTION
'JSON veri sözlüğü boş döndü. dbo.Sas_H_ListFormFieldsDictionaryJson çıktısını kontrol edin.'
USING ERRCODE = 'P0001';
IF to_regprocedure('public."Sas_H_ListFormFieldsDictionaryJson"(varchar, varchar, varchar, boolean, varchar)') IS NULL THEN
RAISE EXCEPTION 'public.Sas_H_ListFormFieldsDictionaryJson bulunamadı. Önce mevcut JSON üreten SP kurulmalıdır.'
USING ERRCODE = 'P0001', DETAIL = '51000';
END IF;
SELECT r."ListFormDataDictionaryJson"
INTO v_JsonData
FROM public."Sas_H_ListFormFieldsDictionaryJson"(
p_DefinitionCultureName,
p_UiCultureName,
p_ListFormCode,
p_OnlyBaseDefinitions,
p_ResourceName) r
WHERE NULLIF(BTRIM(r."ListFormDataDictionaryJson"), '') IS NOT NULL
LIMIT 1;
/*
CacheKey oluşturulması
*/
v_cache_key :=
'ListFormJson|'
|| COALESCE(p_tenant_name, '')
|| '|'
|| COALESCE(p_definition_culture_name, '')
|| '|'
|| COALESCE(p_ui_culture_name, '')
|| '|'
|| COALESCE(p_list_form_code, '')
|| '|'
|| CASE
WHEN COALESCE(p_only_base_definitions, TRUE)
THEN '1'
ELSE '0'
END
|| '|'
|| COALESCE(p_resource_name, '')
|| '|'
|| COALESCE(p_max_forms, 500)::TEXT;
/*
Form sayısı
JSON:
{
"listForms": [...]
}
*/
IF jsonb_typeof(v_json_data -> 'listForms') = 'array' THEN
v_form_count :=
jsonb_array_length(v_json_data -> 'listForms');
IF v_JsonData IS NULL THEN
RAISE EXCEPTION 'JSON veri sözlüğü boş döndü. public.Sas_H_ListFormFieldsDictionaryJson çıktısını kontrol edin.'
USING ERRCODE = 'P0001', DETAIL = '51001';
END IF;
IF NOT (v_JsonData IS JSON OBJECT OR v_JsonData IS JSON ARRAY) THEN
RAISE EXCEPTION 'Üretilen JsonData geçerli JSON değil.'
USING ERRCODE = 'P0001', DETAIL = '51002';
END IF;
/*
Toplam Field sayısı
v_Json := v_JsonData::jsonb;
Her listForms elemanının:
{
"fields": [...]
}
v_CacheKey := concat(
'ListFormJson|',
COALESCE(p_TenantName, ''), '|',
COALESCE(p_DefinitionCultureName, ''), '|',
COALESCE(p_UiCultureName, ''), '|',
COALESCE(p_ListFormCode, ''), '|',
CASE WHEN COALESCE(p_OnlyBaseDefinitions, TRUE) THEN '1' ELSE '0' END, '|',
COALESCE(p_ResourceName, ''), '|',
COALESCE(p_MaxForms, 500)::text
);
alanındaki kayıtlar sayılır.
*/
SELECT COUNT(*)::INTEGER
INTO v_field_count
FROM jsonb_array_elements(
COALESCE(
v_json_data -> 'listForms',
'[]'::JSONB
)
) AS lf
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(lf -> 'fields') = 'array'
THEN lf -> 'fields'
ELSE '[]'::JSONB
END
) AS ff;
v_JsonLength := length(v_JsonData);
IF jsonb_typeof(v_Json -> 'listForms') = 'array' THEN
v_FormCount := jsonb_array_length(v_Json -> 'listForms');
/*
JSON karakter uzunluğu
*/
v_json_length := LENGTH(v_json_data::TEXT);
SELECT COUNT(1)
INTO v_FieldCount
FROM jsonb_array_elements(v_Json -> 'listForms') lf
CROSS JOIN LATERAL jsonb_array_elements(
CASE WHEN jsonb_typeof(lf -> 'fields') = 'array' THEN lf -> 'fields' ELSE '[]'::jsonb END
) ff;
END IF;
/*
PostgreSQL function çağrısı kendi transaction'ı içerisinde
çalıştığı için SQL Server'daki BEGIN TRAN / COMMIT burada
ayrıca kullanılmaz.
*/
/*
Önceki aktif kaydı pasif yap
*/
IF p_deactivate_previous THEN
UPDATE dbo."Sas_H_DbTableColumn"
IF p_DeactivatePrevious THEN
UPDATE public."Sas_H_DbTableColumn"
SET "IsActive" = FALSE
WHERE "DictionaryType" = 'ListFormJson'
AND "CacheKey" = v_cache_key
AND "CacheKey" = v_CacheKey
AND COALESCE("IsActive", FALSE) = TRUE;
END IF;
/*
Yeni JSON kaydını ekle
*/
INSERT INTO dbo."Sas_H_DbTableColumn"
INSERT INTO public."Sas_H_DbTableColumn"
(
"TenantName",
"DictionaryType",
@ -186,54 +104,23 @@ BEGIN
)
VALUES
(
COALESCE(p_tenant_name, 'Sozsoft'),
COALESCE(p_TenantName, 'Sozsoft'),
'ListFormJson',
COALESCE(p_definition_culture_name, 'en'),
COALESCE(p_ui_culture_name, 'tr'),
p_list_form_code,
COALESCE(p_only_base_definitions, TRUE),
p_resource_name,
COALESCE(p_max_forms, 500),
v_cache_key,
v_json_data,
v_form_count,
v_field_count,
v_json_length,
COALESCE(p_DefinitionCultureName, 'en'),
COALESCE(p_UiCultureName, 'tr'),
p_ListFormCode,
COALESCE(p_OnlyBaseDefinitions, TRUE),
p_ResourceName,
COALESCE(p_MaxForms, 500),
v_CacheKey,
v_JsonData,
v_FormCount,
v_FieldCount,
v_JsonLength,
TRUE,
CURRENT_TIMESTAMP,
'dbo.Sas_H_ListFormFieldsDictionaryJson',
p_note
LOCALTIMESTAMP,
'public.Sas_H_ListFormFieldsDictionaryJson',
p_Note
);
/*
Eklenen aktif kaydı döndür
*/
RETURN QUERY
SELECT
T."TenantName",
T."DictionaryType",
T."DefinitionCultureName",
T."UiCultureName",
T."ListFormCode",
T."OnlyBaseDefinitions",
T."ResourceName",
T."MaxForms",
T."CacheKey",
T."FormCount",
T."FieldCount",
T."JsonLength",
T."IsActive",
T."RefreshedAt",
T."SourceProcedure",
T."Note"
FROM dbo."Sas_H_DbTableColumn" T
WHERE T."DictionaryType" = 'ListFormJson'
AND T."CacheKey" = v_cache_key
AND T."IsActive" = TRUE
ORDER BY T."RefreshedAt" DESC
LIMIT 1;
END;
$$;

View file

@ -1,4 +1,7 @@
DO $$
-- HangFire tablolari Hangfire.PostgreSql depolamasindan gelir ("hangfire" semasi, kucuk harfli ad
-- ve kolonlar). Sema HttpApi.Host ilk acildiginda kurulur; DbMigrator kurmadigi icin ilk migrate'te
-- tablolar yoksa view atlanir ve sonraki migrate'te olusturulur.
DO $$
BEGIN
IF to_regclass('hangfire.job') IS NULL
OR to_regclass('hangfire.jobparameter') IS NULL
@ -11,46 +14,61 @@ BEGIN
CREATE OR REPLACE VIEW public."Sas_H_BackgroundWorker_JobFlow" AS
SELECT
j.id AS "Id",
-- Kiraci kapsamli isin adi WorkerType:Name#TenantId biciminde kurulur; ekranda is adi sade
-- kalsin, kiraci ayri kolonda gorunsun diye ek burada ayrilir.
CASE
WHEN position('#' in COALESCE(p."RecurringJobId", '')) > 0
THEN split_part(p."RecurringJobId", '#', 1)
WHEN strpos(COALESCE(p."RecurringJobId", ''), '#') > 0
THEN left(p."RecurringJobId", strpos(p."RecurringJobId", '#') - 1)
ELSE COALESCE(p."RecurringJobId", 'OneTimeJob')
END AS "JobName",
t."Name" AS "Tenant",
COALESCE(prc."ProcessingAt", enq."EnqueuedAt", j.createdat) AS "ExecutionDate",
CASE
WHEN suc."DurationMs" IS NOT NULL THEN suc."DurationMs"
WHEN suc."DurationMs" IS NOT NULL
THEN suc."DurationMs"
WHEN prc."ProcessingAt" IS NOT NULL AND suc."SucceededAt" IS NOT NULL
THEN (EXTRACT(EPOCH FROM (suc."SucceededAt" - prc."ProcessingAt")) * 1000)::bigint
WHEN prc."ProcessingAt" IS NOT NULL AND fld."FailedAt" IS NOT NULL
THEN (EXTRACT(EPOCH FROM (fld."FailedAt" - prc."ProcessingAt")) * 1000)::bigint
ELSE NULL
END AS "Duration",
j.statename AS "Status",
CASE
WHEN j.statename = 'Failed' THEN COALESCE(
fld."ExceptionMessage",
fld."FailedReason",
fld."ExceptionType",
'Unknown error'
)
WHEN j.statename = 'Failed'
THEN COALESCE(
fld."ExceptionMessage",
fld."FailedReason",
fld."ExceptionType",
'Unknown error'
)
ELSE NULL
END AS "FailureReason",
fld."ExceptionType",
fld."ExceptionDetails"
fld."ExceptionType" AS "ExceptionType",
fld."ExceptionDetails" AS "ExceptionDetails"
FROM hangfire.job j
LEFT JOIN LATERAL
(
SELECT
MAX(CASE
WHEN jp.name = 'RecurringJobId'
THEN REPLACE(jp.value, '"', '')
END) AS "RecurringJobId"
WHEN jp.name = 'RecurringJobId'
THEN REPLACE(jp.value, '"', '')
END) AS "RecurringJobId"
FROM hangfire.jobparameter jp
WHERE jp.jobid = j.id
) p ON TRUE
LEFT JOIN LATERAL
(
SELECT
@ -61,6 +79,7 @@ LEFT JOIN LATERAL
ORDER BY s.createdat DESC
LIMIT 1
) enq ON TRUE
LEFT JOIN LATERAL
(
SELECT
@ -71,17 +90,22 @@ LEFT JOIN LATERAL
ORDER BY s.createdat DESC
LIMIT 1
) prc ON TRUE
LEFT JOIN LATERAL
(
SELECT
s.createdat AS "SucceededAt",
NULLIF(s.data::jsonb ->> 'PerformanceDuration', '')::bigint AS "DurationMs"
CASE
WHEN BTRIM(s.data::jsonb ->> 'PerformanceDuration') ~ '^[+-]?[0-9]{1,18}$'
THEN BTRIM(s.data::jsonb ->> 'PerformanceDuration')::bigint
END AS "DurationMs"
FROM hangfire.state s
WHERE s.jobid = j.id
AND s.name = 'Succeeded'
ORDER BY s.createdat DESC
LIMIT 1
) suc ON TRUE
LEFT JOIN LATERAL
(
SELECT
@ -96,17 +120,22 @@ LEFT JOIN LATERAL
ORDER BY s.createdat DESC
LIMIT 1
) fld ON TRUE
LEFT JOIN LATERAL
(
SELECT CASE
WHEN length(split_part(p."RecurringJobId", '#', 2)) = 36
AND split_part(p."RecurringJobId", '#', 2) ~ '^[0-9a-fA-F-]+$'
THEN split_part(p."RecurringJobId", '#', 2)::uuid
END AS "TenantId"
SELECT
CASE
WHEN strpos(COALESCE(p."RecurringJobId", ''), '#') > 0
AND substring(p."RecurringJobId" FROM strpos(p."RecurringJobId", '#') + 1 FOR 36)
~ '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$'
THEN substring(p."RecurringJobId" FROM strpos(p."RecurringJobId", '#') + 1 FOR 36)::uuid
END AS "TenantId"
) tn ON TRUE
LEFT JOIN public."AbpTenants" t
ON t."Id" = tn."TenantId"
WHERE COALESCE(j.invocationdata::jsonb ->> 'Method', '') <> 'DoWorkAsync'
WHERE j.invocationdata::jsonb ->> 'Method' <> 'DoWorkAsync'
$view$;
END;
$$;

View file

@ -0,0 +1,59 @@
CREATE TABLE IF NOT EXISTS public."Adm_T_Contact"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Logo" text NULL,
"Name" varchar(256) NOT NULL,
"CompanyType" varchar(16) NOT NULL DEFAULT 'Company',
"Email" varchar(256) NULL,
"Phone" varchar(32) NULL,
"Website" varchar(256) NULL,
"Address1" varchar(256) NULL,
"Address2" varchar(256) NULL,
"Country" varchar(128) NULL,
"City" varchar(128) NULL,
"District" varchar(128) NULL,
"Township" varchar(128) NULL,
"PostalCode" varchar(16) NULL,
"TaxNumber" varchar(32) NULL,
"TaxOffice" varchar(128) NULL,
"JobPosition" varchar(128) NULL,
"CultureName" varchar(16) NULL,
"Tags" varchar(512) NULL,
"SalesUserId" uuid NULL,
"SalesCurrency" varchar(16) NULL,
"SalesPaymentTermId" uuid NULL,
"SalesPaymentMethodId" uuid NULL,
"DeliveryMethodId" uuid NULL,
"SalesIncotermId" uuid NULL,
"SalesIncotermLocation" varchar(128) NULL,
"RfqGrouping" varchar(16) NOT NULL DEFAULT 'OnOrder',
"BuyerUserId" uuid NULL,
"PurchasePaymentTermId" uuid NULL,
"PurchasePaymentMethodId" uuid NULL,
"ReceiptReminder" boolean NOT NULL DEFAULT FALSE,
"ReminderDaysBefore" integer NULL DEFAULT 1,
"SupplierCurrency" varchar(16) NULL,
"PurchaseIncotermId" uuid NULL,
"PurchaseIncotermLocation" varchar(128) NULL,
"FinancialStatus" varchar(16) NULL,
"Reference" varchar(64) NULL,
"Barcode" varchar(64) NULL,
"SectorId" uuid NULL,
"ReceivableAccount" varchar(64) NULL,
"PayableAccount" varchar(64) NULL,
"InvoiceAutoApprove" varchar(16) NULL DEFAULT 'AfterThree',
"PaymentQrType" varchar(16) NULL,
"Latitude" double precision NULL,
"Longitude" double precision NULL,
"Notes" text NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Adm_T_Contact" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS public."Adm_T_ContactBankAccount"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ContactId" uuid NOT NULL,
"BankName" varchar(128) NULL,
"Iban" varchar(34) NOT NULL,
"AccountHolder" varchar(256) NULL,
"Currency" varchar(16) NULL,
"IsDefault" boolean NOT NULL DEFAULT FALSE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Adm_T_ContactBankAccount" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public."Adm_T_ContactPerson"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ContactId" uuid NOT NULL,
"Photo" text NULL,
"Name" varchar(256) NOT NULL,
"JobTitle" varchar(128) NULL,
"Email" varchar(256) NULL,
"Phone" varchar(32) NULL,
"Mobile" varchar(32) NULL,
"Notes" text NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Adm_T_ContactPerson" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS public."Adm_T_ContactTag"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"Color" varchar(16) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Adm_T_ContactTag" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,25 @@
CREATE OR REPLACE FUNCTION public."Hr_F_HrSetting"
(
p_TenantId uuid,
p_Name varchar(128)
)
RETURNS varchar(2048)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
-- Ayar ekranının çözüm sırası (T|G|D): kiracı değeri, genel değer, tanımdaki varsayılan.
-- ProviderKey Guid metnidir; SQL Server karşılaştırması büyük/küçük harf duyarsız olduğundan lower().
RETURN COALESCE(
(SELECT s."Value" FROM "AbpSettings" s
WHERE s."Name" = p_Name AND s."ProviderName" = 'T' AND p_TenantId IS NOT NULL
AND lower(s."ProviderKey") = p_TenantId::text
LIMIT 1),
(SELECT s."Value" FROM "AbpSettings" s
WHERE s."Name" = p_Name AND s."ProviderName" = 'G'
LIMIT 1),
(SELECT d."DefaultValue" FROM "Sas_H_SettingDefinition" d
WHERE d."Code" = p_Name AND d."IsDeleted" = FALSE
LIMIT 1));
END;
$$;

View file

@ -0,0 +1,37 @@
CREATE OR REPLACE FUNCTION public."Hr_F_LeaveNetDays"
(
p_TenantId uuid,
p_EmployeeId uuid,
p_StartDate date,
p_EndDate date
)
RETURNS numeric(9, 1)
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_Total numeric(9, 1);
v_Day date := p_StartDate;
BEGIN
-- İzin talebinin düştüğü gün sayısı: çalışma günleri (Hr_F_LeaveWorkDays) eksi çalışanın
-- rapor (istirahat) kapsamındaki günleri. İzin sırasında alınan raporun günleri izinden sayılmaz;
-- puantajda da o günler Raporlu'dur, bakiye ile puantaj aynı günü iki kez kullanmaz.
IF p_StartDate IS NULL OR p_EndDate IS NULL OR p_EndDate < p_StartDate THEN
RETURN 0;
END IF;
v_Total := public."Hr_F_LeaveWorkDays"(p_TenantId, p_StartDate, p_EndDate);
WHILE v_Day <= p_EndDate AND v_Total > 0 LOOP
IF EXISTS (SELECT 1 FROM "Adm_T_UserSickLeave" s
WHERE s."UserId" = p_EmployeeId AND s."IsDeleted" = FALSE
AND v_Day BETWEEN s."StartDate"::date AND s."EndDate"::date) THEN
v_Total := v_Total - public."Hr_F_LeaveWorkDays"(p_TenantId, v_Day, v_Day);
END IF;
v_Day := v_Day + 1;
END LOOP;
RETURN CASE WHEN v_Total < 0 THEN 0 ELSE v_Total END;
END;
$$;

View file

@ -0,0 +1,76 @@
CREATE OR REPLACE FUNCTION public."Hr_F_LeaveWorkDays"
(
p_TenantId uuid,
p_StartDate date,
p_EndDate date
)
RETURNS numeric(9, 1)
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_Work boolean[];
v_Total numeric(9, 1) := 0;
v_Day date := p_StartDate;
v_Value numeric(3, 1);
v_Calendar numeric(3, 1);
BEGIN
-- İzin günü = ayarlarda çalışma günü işaretli olup Çalışma Takvimi'nde tatil olmayan gün.
-- Yarım gün (arife) 0,5 sayılır. Yıllık tekrarlı satır başlangıç yılından itibaren her yıl
-- aynı gün/ay aralığına düşer (29 Şubat artık olmayan yılda 28 Şubat'a kayar).
IF p_StartDate IS NULL OR p_EndDate IS NULL OR p_EndDate < p_StartDate THEN
RETURN 0;
END IF;
-- Gün sırası Pazartesi = 1 (ISODOW). Ayar tanımı yoksa Pzt–Cum.
SELECT array_agg(
CASE
WHEN s.v IS NULL THEN d.DayIndex <= 5
WHEN lower(s.v) = 'true' THEN TRUE
ELSE FALSE
END
ORDER BY d.DayIndex)
INTO v_Work
FROM (VALUES
(1, 'App.HrSettings.Leave.WorkMonday'),
(2, 'App.HrSettings.Leave.WorkTuesday'),
(3, 'App.HrSettings.Leave.WorkWednesday'),
(4, 'App.HrSettings.Leave.WorkThursday'),
(5, 'App.HrSettings.Leave.WorkFriday'),
(6, 'App.HrSettings.Leave.WorkSaturday'),
(7, 'App.HrSettings.Leave.WorkSunday')) d (DayIndex, SettingName)
CROSS JOIN LATERAL (SELECT public."Hr_F_HrSetting"(p_TenantId, d.SettingName) AS v) s;
WHILE v_Day <= p_EndDate LOOP
v_Value := 0;
IF v_Work[EXTRACT(ISODOW FROM v_Day)::int] THEN
v_Value := 1;
SELECT CASE WHEN MAX(CASE WHEN c."DayType" = 'Holiday' THEN 1 ELSE 0 END) = 1 THEN 0 ELSE 0.5 END
INTO v_Calendar
FROM "Hr_T_WorkCalendar" c
WHERE c."IsDeleted" = FALSE AND c."IsActive" = TRUE
AND c."TenantId" IS NOT DISTINCT FROM p_TenantId
AND (
((c."RecurrenceRule" IS NULL OR c."RecurrenceRule" = '') AND v_Day BETWEEN c."StartDate" AND c."EndDate")
OR (c."RecurrenceRule" = 'FREQ=YEARLY'
AND EXTRACT(YEAR FROM v_Day) >= EXTRACT(YEAR FROM c."StartDate")
AND v_Day BETWEEN (c."StartDate" + make_interval(years => (EXTRACT(YEAR FROM v_Day) - EXTRACT(YEAR FROM c."StartDate"))::int))::date
AND (c."EndDate" + make_interval(years => (EXTRACT(YEAR FROM v_Day) - EXTRACT(YEAR FROM c."StartDate"))::int))::date)
)
HAVING COUNT(*) > 0;
-- Takvimde satır yoksa gün tam sayılır (SELECT INTO satırsızken değişkeni NULL yapar).
IF FOUND THEN
v_Value := v_Calendar;
END IF;
END IF;
v_Total := v_Total + v_Value;
v_Day := v_Day + 1;
END LOOP;
RETURN v_Total;
END;
$$;

View file

@ -0,0 +1,26 @@
CREATE OR REPLACE FUNCTION public."Hr_F_TimesheetPeriodClosed"
(
p_TenantId uuid,
p_StartDate date,
p_EndDate date
)
RETURNS boolean
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
-- Aralığın değdiği aylardan biri kapalı puantaj dönemiyse TRUE. Puantajı değiştiren kayıtlar
-- (izin, rapor, puantaj günü) kapalı aya yazılamaz; dönemi İK açar, açılış ayı yeniden hesaplar.
IF p_StartDate IS NULL OR p_EndDate IS NULL THEN
RETURN FALSE;
END IF;
RETURN EXISTS (
SELECT 1 FROM "Hr_T_TimesheetPeriod" p
WHERE p."IsDeleted" = FALSE AND p."Status" = 'Closed'
AND p."TenantId" IS NOT DISTINCT FROM p_TenantId
AND p."PeriodYear" * 100 + p."PeriodMonth"
BETWEEN EXTRACT(YEAR FROM p_StartDate)::int * 100 + EXTRACT(MONTH FROM p_StartDate)::int
AND EXTRACT(YEAR FROM p_EndDate)::int * 100 + EXTRACT(MONTH FROM p_EndDate)::int);
END;
$$;

View file

@ -0,0 +1,28 @@
CREATE OR REPLACE FUNCTION public."Hr_F_WorkCalendarDayType"
(
p_TenantId uuid,
p_Day date
)
RETURNS varchar(16)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
-- Çalışma Takvimi'nde güne düşen tür: Holiday (tam gün tatil) yarım günü (HalfDay) ezer; yoksa NULL.
-- Yıllık tekrarlı satır eşlemesi Hr_F_LeaveWorkDays ile aynıdır.
RETURN (
SELECT CASE WHEN MAX(CASE WHEN c."DayType" = 'Holiday' THEN 1 ELSE 0 END) = 1 THEN 'Holiday' ELSE 'HalfDay' END
FROM "Hr_T_WorkCalendar" c
WHERE c."IsDeleted" = FALSE AND c."IsActive" = TRUE
AND c."TenantId" IS NOT DISTINCT FROM p_TenantId
AND (
((c."RecurrenceRule" IS NULL OR c."RecurrenceRule" = '') AND p_Day BETWEEN c."StartDate"::date AND c."EndDate"::date)
OR (c."RecurrenceRule" = 'FREQ=YEARLY'
AND EXTRACT(YEAR FROM p_Day) >= EXTRACT(YEAR FROM c."StartDate")
AND p_Day BETWEEN (c."StartDate" + make_interval(years => (EXTRACT(YEAR FROM p_Day) - EXTRACT(YEAR FROM c."StartDate"))::int))::date
AND (c."EndDate" + make_interval(years => (EXTRACT(YEAR FROM p_Day) - EXTRACT(YEAR FROM c."StartDate"))::int))::date)
)
HAVING COUNT(*) > 0
);
END;
$$;

View file

@ -0,0 +1,125 @@
-- Custom Endpoint sonucu (Received/Inserted/Skipped/Errors) okuduğu için prosedür değil, satır dönen fonksiyondur.
CREATE OR REPLACE FUNCTION public."Hr_S_AttendanceImport"(
p_TenantId varchar(64),
p_UserId varchar(64),
p_Items text
)
RETURNS TABLE ("Received" integer, "Inserted" integer, "Skipped" integer, "Errors" integer)
LANGUAGE plpgsql
AS $$
DECLARE
v_Tenant uuid;
v_Actor uuid;
v_Json jsonb;
v_Item jsonb;
v_EventText text;
v_EventTime timestamp;
v_Received integer;
v_Inserted integer;
v_Errors integer;
BEGIN
-- PDKS ucu (Custom Endpoint POST /hr/attendance/): gövdedeki Items JSON dizisini hareket olarak
-- yazar. Her öğe: CardNo (kullanıcı kartındaki PDKS Kart No), RegistrationNo (Sicil No) ya da UserName;
-- EventTime (yerel saat, ISO 8601), Direction (In/Out/Auto), DeviceCode. Kart/sicil eşlemesini
-- Hr_S_AttendanceProcess yapar. Aynı kişi ve aynı zamanla daha önce gelmiş okutma atlanır; kapalı
-- döneme düşen ve kartı tanımsız okutma hatalı işaretlenir, gönderimi durdurmaz.
BEGIN
v_Tenant := p_TenantId::uuid;
EXCEPTION WHEN invalid_text_representation THEN
v_Tenant := NULL;
END;
BEGIN
v_Actor := p_UserId::uuid;
EXCEPTION WHEN invalid_text_representation THEN
v_Actor := NULL;
END;
-- ISJSON yalnız dizi ve nesneyi geçerli sayar; nesne tek öğe olarak okunur (OPENJSON ... WITH gibi).
IF p_Items IS NOT NULL THEN
BEGIN
v_Json := p_Items::jsonb;
EXCEPTION WHEN others THEN
v_Json := NULL;
END;
END IF;
IF v_Json IS NULL OR jsonb_typeof(v_Json) NOT IN ('array', 'object') THEN
RAISE EXCEPTION 'Items geçerli bir JSON dizisi olmalıdır.' USING ERRCODE = 'P0001', DETAIL = '51210';
END IF;
IF jsonb_typeof(v_Json) = 'object' THEN
v_Json := jsonb_build_array(v_Json);
END IF;
DROP TABLE IF EXISTS ai_item;
CREATE TEMP TABLE ai_item
(
CardNo varchar(64) NULL, UserName varchar(256) NULL, EventTime timestamp NULL,
Direction varchar(8) NULL, DeviceCode varchar(64) NULL, EmployeeId uuid NULL
) ON COMMIT DROP;
DROP TABLE IF EXISTS ai_inserted;
CREATE TEMP TABLE ai_inserted (Id uuid) ON COMMIT DROP;
FOR v_Item IN SELECT e FROM jsonb_array_elements(v_Json) e LOOP
-- Dizideki skaler öğe OPENJSON ... WITH'te bütün alanları boş bir satırdır.
IF jsonb_typeof(v_Item) <> 'object' THEN
v_Item := '{}'::jsonb;
END IF;
v_EventText := v_Item ->> 'EventTime';
v_EventTime := NULL;
IF v_EventText IS NOT NULL THEN
BEGIN
v_EventTime := v_EventText::timestamp;
EXCEPTION WHEN others THEN
v_EventTime := NULL;
END;
END IF;
INSERT INTO ai_item (CardNo, UserName, EventTime, Direction, DeviceCode)
VALUES (
COALESCE(NULLIF(btrim(left(v_Item ->> 'CardNo', 64)), ''), NULLIF(btrim(left(v_Item ->> 'RegistrationNo', 64)), '')),
NULLIF(btrim(left(v_Item ->> 'UserName', 256)), ''),
v_EventTime,
CASE WHEN lower(v_Item ->> 'Direction') IN ('in', 'out') THEN left(v_Item ->> 'Direction', 8) ELSE 'Auto' END,
left(v_Item ->> 'DeviceCode', 64));
END LOOP;
v_Received := (SELECT COUNT(*) FROM ai_item)::integer;
DELETE FROM ai_item WHERE EventTime IS NULL OR (CardNo IS NULL AND UserName IS NULL);
UPDATE ai_item i
SET EmployeeId = u."Id"
FROM public."AbpUsers" u
WHERE lower(u."UserName") = lower(i.UserName) AND u."IsDeleted" = FALSE
AND (u."TenantId" = v_Tenant OR (u."TenantId" IS NULL AND v_Tenant IS NULL))
AND i.UserName IS NOT NULL;
WITH ins AS (
INSERT INTO public."Hr_T_AttendanceLog"
("Id", "TenantId", "EmployeeId", "CardNo", "EventTime", "Direction", "Source", "DeviceCode", "IsProcessed", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), v_Tenant, n.EmployeeId, n.CardNo, n.EventTime, n.Direction, 'Pdks', n.DeviceCode, FALSE,
(NOW() AT TIME ZONE 'UTC'), v_Actor, FALSE
FROM (
SELECT DISTINCT a.EmployeeId, a.CardNo, a.EventTime, a.Direction, a.DeviceCode FROM ai_item a
) n
WHERE NOT EXISTS (
SELECT 1 FROM public."Hr_T_AttendanceLog" l
WHERE l."IsDeleted" = FALSE AND l."EventTime" = n.EventTime
AND (l."TenantId" = v_Tenant OR (l."TenantId" IS NULL AND v_Tenant IS NULL))
AND ((n.EmployeeId IS NOT NULL AND l."EmployeeId" = n.EmployeeId)
OR (n.EmployeeId IS NULL AND lower(l."CardNo") = lower(n.CardNo))))
RETURNING "Id"
)
INSERT INTO ai_inserted (Id)
SELECT ins."Id" FROM ins;
CALL public."Hr_S_AttendanceProcess"(FALSE);
v_Inserted := (SELECT COUNT(*) FROM ai_inserted)::integer;
v_Errors := (SELECT COUNT(*) FROM public."Hr_T_AttendanceLog" l INNER JOIN ai_inserted i ON i.Id = l."Id"
WHERE l."ProcessError" IS NOT NULL)::integer;
RETURN QUERY SELECT v_Received, v_Inserted, v_Received - v_Inserted, v_Errors;
END;
$$;

View file

@ -0,0 +1,97 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_AttendanceProcess"(
p_FailOnClosed boolean DEFAULT TRUE
)
LANGUAGE plpgsql
AS $$
DECLARE
v_Key record;
BEGIN
-- İşlenmemiş hareketleri (ekran, Excel import, PDKS ucu; silinenler dahil) işler: çalışanı kullanıcı
-- kartındaki PDKS Kart No'dan (yoksa Sicil No'dan) bulur, hareketin iş gününü vardiyaya göre belirler ve etkilenen günlerin puantajını
-- yeniden hesaplar. Ekrandan gelen değişiklik kapalı döneme düşerse kayıt geri alınır
-- (p_FailOnClosed = TRUE); PDKS ve gece işinde satır hatalı işaretlenir.
-- Kart/sicil eşleşmesi SQL Server harmanlaması gibi büyük/küçük harf duyarsızdır.
DROP TABLE IF EXISTS ap_log;
CREATE TEMP TABLE ap_log
(
Id uuid PRIMARY KEY, TenantId uuid NULL, EmployeeId uuid NULL,
CardNo varchar(64) NULL, EventTime timestamp, IsDeleted boolean, OldWorkDate date NULL,
NewWorkDate date NULL, Error varchar(32) NULL
) ON COMMIT DROP;
INSERT INTO ap_log (Id, TenantId, EmployeeId, CardNo, EventTime, IsDeleted, OldWorkDate)
SELECT "Id", "TenantId", "EmployeeId", "CardNo", "EventTime", "IsDeleted", "WorkDate"
FROM public."Hr_T_AttendanceLog"
WHERE "IsProcessed" = FALSE;
IF NOT EXISTS (SELECT 1 FROM ap_log) THEN
RETURN;
END IF;
UPDATE ap_log a
SET EmployeeId = u.Id
FROM (
SELECT a2.Id AS LogId, (
SELECT x."Id"
FROM public."AbpUsers" x
WHERE (lower(x."PdksCardNo") = lower(a2.CardNo) OR lower(x."RegistrationNo") = lower(a2.CardNo)) AND x."IsDeleted" = FALSE
AND (x."TenantId" = a2.TenantId OR (x."TenantId" IS NULL AND a2.TenantId IS NULL))
ORDER BY CASE WHEN lower(x."PdksCardNo") = lower(a2.CardNo) THEN 0 ELSE 1 END, x."IsActive" DESC NULLS LAST
LIMIT 1) AS Id
FROM ap_log a2
WHERE a2.EmployeeId IS NULL AND a2.CardNo IS NOT NULL AND a2.CardNo <> ''
) u
WHERE u.LogId = a.Id AND u.Id IS NOT NULL;
UPDATE ap_log SET Error = 'UnknownCard' WHERE EmployeeId IS NULL AND IsDeleted = FALSE;
UPDATE ap_log a
SET NewWorkDate = (a.EventTime - s."DayOffset" * INTERVAL '1 minute')::date
FROM public."Hr_V_AttendanceShift" s
WHERE s."EmployeeId" = a.EmployeeId
AND a.IsDeleted = FALSE AND a.Error IS NULL;
-- Kapalı dönem: hareketin eski ya da yeni günü kapalı aya düşüyorsa.
UPDATE ap_log a
SET Error = 'ClosedPeriod'
WHERE EXISTS (
SELECT 1 FROM public."Hr_T_TimesheetPeriod" p
WHERE p."IsDeleted" = FALSE AND p."Status" = 'Closed'
AND (p."TenantId" = a.TenantId OR (p."TenantId" IS NULL AND a.TenantId IS NULL))
AND ((p."PeriodYear" = EXTRACT(YEAR FROM a.OldWorkDate)::integer AND p."PeriodMonth" = EXTRACT(MONTH FROM a.OldWorkDate)::integer)
OR (p."PeriodYear" = EXTRACT(YEAR FROM a.NewWorkDate)::integer AND p."PeriodMonth" = EXTRACT(MONTH FROM a.NewWorkDate)::integer)));
IF p_FailOnClosed = TRUE AND EXISTS (SELECT 1 FROM ap_log WHERE Error = 'ClosedPeriod') THEN
RAISE EXCEPTION 'Kapalı puantaj dönemine ait hareket eklenemez, değiştirilemez ya da silinemez. Dönemi İnsan Kaynakları açabilir.'
USING ERRCODE = 'P0001', DETAIL = '51200';
END IF;
UPDATE public."Hr_T_AttendanceLog" l
SET "EmployeeId" = a.EmployeeId,
"WorkDate" = CASE WHEN a.Error = 'ClosedPeriod' THEN l."WorkDate" ELSE a.NewWorkDate END,
"ProcessError" = a.Error,
"Source" = COALESCE(l."Source", 'Import'),
"IsProcessed" = TRUE
FROM ap_log a
WHERE a.Id = l."Id";
-- Etkilenen günler: hareketin eski günü (taşındıysa ya da silindiyse) ve yeni günü.
DROP TABLE IF EXISTS ap_keys;
CREATE TEMP TABLE ap_keys (TenantId uuid NULL, EmployeeId uuid, FromDate date, ToDate date) ON COMMIT DROP;
INSERT INTO ap_keys (TenantId, EmployeeId, FromDate, ToDate)
SELECT k.TenantId, k.EmployeeId, MIN(k.D), MAX(k.D)
FROM (
SELECT TenantId, EmployeeId, OldWorkDate AS D FROM ap_log
WHERE EmployeeId IS NOT NULL AND OldWorkDate IS NOT NULL AND COALESCE(Error, '') <> 'ClosedPeriod'
UNION ALL
SELECT TenantId, EmployeeId, NewWorkDate FROM ap_log
WHERE EmployeeId IS NOT NULL AND NewWorkDate IS NOT NULL AND Error IS NULL
) k
GROUP BY k.TenantId, k.EmployeeId;
FOR v_Key IN SELECT TenantId, EmployeeId, FromDate, ToDate FROM ap_keys LOOP
CALL public."Hr_S_TimesheetCalculate"(v_Key.TenantId, v_Key.EmployeeId, v_Key.FromDate, v_Key.ToDate, NULL);
END LOOP;
END;
$$;

View file

@ -0,0 +1,17 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_DevelopmentPlanSync"(
p_PlanId uuid
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Eğitim aksiyonu, çalışan o eğitime "Katıldı" olarak işlendiyse kendiliğinden tamamlanır;
-- tamamlanan aksiyonun ilerlemesi her zaman %100'dür.
UPDATE "Hr_T_DevelopmentPlan" dp
SET "Status" = 'Completed', "Progress" = 100
WHERE dp."Id" = p_PlanId AND dp."IsDeleted" = FALSE AND dp."Status" <> 'Cancelled'
AND (dp."Status" = 'Completed' OR EXISTS (
SELECT 1 FROM "Hr_T_TrainingParticipant" tp
WHERE tp."TrainingId" = dp."TrainingId" AND tp."UserId" = dp."EmployeeId"
AND tp."AttendanceStatus" = 'Attended' AND tp."IsDeleted" = FALSE));
END
$$;

View file

@ -0,0 +1,26 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_LeaveDaysRefresh"(
p_TenantId uuid,
p_EmployeeId uuid
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Rapor eklenince, tarihi değişince ya da silinince çalışanın diğer izin taleplerinin gün sayısı
-- yeniden hesaplanır (Hr_F_LeaveNetDays): rapora denk gelen yıllık izin günü bakiyeye geri döner.
-- Rapor türündeki talep kendi günlerini sayar; onun gün sayısına dokunulmaz.
UPDATE public."Hr_T_LeaveRequest" AS r
SET "Days" = s."NewDays"
FROM (
SELECT x."Id",
CASE WHEN x."IsHalfDay" = TRUE AND n."Days" > 0.5 THEN 0.5 ELSE n."Days" END AS "NewDays"
FROM public."Hr_T_LeaveRequest" x
INNER JOIN public."Hr_T_LeaveType" t ON t."Id" = x."LeaveTypeId"
CROSS JOIN LATERAL (
SELECT public."Hr_F_LeaveNetDays"(p_TenantId, x."EmployeeId", x."StartDate"::date, x."EndDate"::date) AS "Days"
) n
WHERE x."EmployeeId" = p_EmployeeId AND x."IsDeleted" = FALSE AND t."Code" <> 'Sick'
) s
WHERE r."Id" = s."Id"
AND r."Days" <> s."NewDays";
END
$$;

View file

@ -0,0 +1,44 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_LeaveNotify"(
p_RequestId uuid,
p_RecipientId uuid,
p_TypeName varchar(64),
p_SubjectKey varchar(128),
p_MessageKey varchar(128),
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Alıcı talepten gelir (müdür, İK onaycısı ya da çalışan); tipin Custom kuralları yalnızca
-- kanalı belirler. Metin TR dil anahtarından okunur, yer tutucular talepten dolar.
IF p_RequestId IS NULL OR p_RecipientId IS NULL THEN
RETURN;
END IF;
INSERT INTO public."Adm_T_Notification" ("Id", "TenantId", "NotificationRuleId", "NotificationChannel", "NotificationTypeId",
"Identifier", "UserId", "Message", "Subject", "IsSent", "IsRead", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), lr."TenantId", r."Id", r."Channel", t."Id",
CASE WHEN r."Channel" = 'Mail' THEN u."Email" ELSE u."Id"::text END,
u."Id",
left(replace(replace(replace(replace(replace(replace(COALESCE(msg."Value", p_MessageKey),
'[Employee]', COALESCE(lr."EmployeeName", '')),
'[LeaveType]', COALESCE(lt."Name", '')),
'[StartDate]', to_char(lr."StartDate", 'DD.MM.YYYY')),
'[EndDate]', to_char(lr."EndDate", 'DD.MM.YYYY')),
'[Days]', rtrim(rtrim(to_char(round(lr."Days", 1), 'FM999999999990.0'), '0'), '.')),
'[Status]', COALESCE(lr."ApprovalStatus", '')), 2048),
left(COALESCE(sub."Value", p_SubjectKey), 256),
(r."Channel" = 'UiActivity'), FALSE, (NOW() AT TIME ZONE 'UTC'), p_ActorId, FALSE
FROM public."Hr_T_LeaveRequest" lr
INNER JOIN public."Hr_T_LeaveType" lt ON lt."Id" = lr."LeaveTypeId"
INNER JOIN public."AbpUsers" u ON u."Id" = p_RecipientId AND u."IsActive" = TRUE
INNER JOIN public."Adm_T_NotificationType" t ON t."Name" = p_TypeName AND t."IsDeleted" = FALSE
AND (t."TenantId" = lr."TenantId" OR (t."TenantId" IS NULL AND lr."TenantId" IS NULL))
INNER JOIN public."Adm_T_NotificationRule" r ON r."NotificationTypeId" = t."Id" AND r."IsActive" = TRUE
AND r."IsCustomized" = FALSE AND r."IsDeleted" = FALSE AND r."RecipientType" = 'Custom' AND r."RecipientId" IS NULL
LEFT JOIN public."Sas_H_LanguageText" msg ON msg."ResourceName" = 'Platform' AND msg."Key" = p_MessageKey AND msg."CultureName" = 'tr'
LEFT JOIN public."Sas_H_LanguageText" sub ON sub."ResourceName" = 'Platform' AND sub."Key" = p_SubjectKey AND sub."CultureName" = 'tr'
WHERE lr."Id" = p_RequestId
AND (r."Channel" <> 'Mail' OR u."Email" IS NOT NULL);
END
$$;

View file

@ -0,0 +1,393 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_LeaveRequestAfterSave"(
p_RequestId uuid,
p_ActorId uuid,
p_Actor varchar(16)
)
LANGUAGE plpgsql
AS $$
DECLARE
v_StatusStart constant varchar(50) := 'Onaya Gönderildi';
v_StatusManager constant varchar(50) := 'Müdür Onayı';
v_StatusHr constant varchar(50) := 'İK Onayı';
v_StatusApproved constant varchar(50) := 'Onaylandı';
v_StatusRejected constant varchar(50) := 'Reddedildi';
v_TenantId uuid;
v_EmployeeId uuid;
v_LeaveTypeId uuid;
v_StartDate timestamp;
v_EndDate timestamp;
v_IsHalfDay boolean;
v_HalfDayPart varchar(16);
v_Document text;
v_SickLeaveId uuid;
v_Status varchar(50);
v_LastStatus varchar(50);
v_Description varchar(1024);
v_IsDeleted boolean;
v_EmployeeName varchar(256);
v_DepartmentId uuid;
v_JobPositionId uuid;
v_Gender varchar(16);
v_HireDate date;
v_TerminationDate date;
v_Positions uuid[];
v_Managers uuid[] := '{}';
v_Level integer := 0;
v_ManagerIds varchar(1024);
v_ManagerUserNames varchar(1024);
v_RequiresManager integer;
v_HrApprover varchar(1024);
v_TypeCode varchar(32);
v_TypeGender varchar(16);
v_MaxDaysPerYear integer;
v_Deducts boolean;
v_RequiresDocument boolean;
v_AllowHalfDay boolean;
v_ExceedAction varchar(16);
v_WorkDays numeric(9, 1);
v_Days numeric(9, 1);
v_Warning varchar(512);
v_Remaining numeric(9, 1);
v_OtherPending numeric(9, 1);
v_UsedThisYear numeric(9, 1);
v_Recipients uuid[];
v_Recipient uuid;
v_Type varchar(64);
v_Subject varchar(128);
v_Body varchar(128);
BEGIN
-- İzin talebinin her kaydından (ekleme, güncelleme, Grid Workflow kararı, silme) sonra çalışır:
-- çalışan bilgisi, talep no, müdür, gün sayısı tamamlanır; kurallar doğrulanır (hata kaydı
-- geri alır); onaylı rapor türü Sağlık Kartı istirahatiyle eşlenir; durum değiştiyse bildirim
-- gider; puantaj yeniden hesaplanır.
SELECT "TenantId", "EmployeeId", "LeaveTypeId", "StartDate", "EndDate", "IsHalfDay", "HalfDayPart",
"DocumentContent", "SickLeaveId", "ApprovalStatus", "LastNotifiedStatus", "Description", "IsDeleted"
INTO v_TenantId, v_EmployeeId, v_LeaveTypeId, v_StartDate, v_EndDate, v_IsHalfDay, v_HalfDayPart,
v_Document, v_SickLeaveId, v_Status, v_LastStatus, v_Description, v_IsDeleted
FROM public."Hr_T_LeaveRequest"
WHERE "Id" = p_RequestId;
IF v_EmployeeId IS NULL THEN
RETURN;
END IF;
-- Puantajı kapanmış aya düşen talep eklenemez, değiştirilemez, silinemez (eski tarihleri
-- Hr_S_LeaveRequestGuard denetler).
IF public."Hr_F_TimesheetPeriodClosed"(v_TenantId, v_StartDate::date, v_EndDate::date) THEN
RAISE EXCEPTION 'İzin tarihleri kapalı bir puantaj dönemine düşüyor. Önce İnsan Kaynakları dönemi açmalıdır.'
USING ERRCODE = 'P0001', DETAIL = '51118';
END IF;
IF v_IsDeleted = TRUE THEN
-- Silinen talebin istirahati de kalkar; rapora denk gelen izin günleri ve puantaj yenilenir.
IF v_SickLeaveId IS NOT NULL THEN
UPDATE public."Adm_T_UserSickLeave"
SET "IsDeleted" = TRUE, "DeletionTime" = (NOW() AT TIME ZONE 'UTC'), "DeleterId" = p_ActorId
WHERE "Id" = v_SickLeaveId AND "IsDeleted" = FALSE;
CALL public."Hr_S_LeaveDaysRefresh"(v_TenantId, v_EmployeeId);
END IF;
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, v_EmployeeId, v_StartDate::date, v_EndDate::date, p_ActorId);
RETURN;
END IF;
---------------------------------------------------------------------------------------------
-- Çalışan ve departman müdürü
---------------------------------------------------------------------------------------------
SELECT CONCAT("Name", ' ', "Surname"), "DepartmentId", "JobPositionId", "Gender",
"HireDate"::date, "TerminationDate"::date
INTO v_EmployeeName, v_DepartmentId, v_JobPositionId, v_Gender, v_HireDate, v_TerminationDate
FROM public."AbpUsers"
WHERE "Id" = v_EmployeeId;
-- Müdür = çalışanın departmanındaki en üst pozisyon(lar)ın sahibi (üst pozisyonu başka
-- departmanda olan ya da üstü olmayan pozisyon). Çalışan bu pozisyondaysa ya da pozisyon boşsa
-- bir üst pozisyona çıkılır.
v_Positions := ARRAY(
SELECT p."Id"
FROM public."Hr_T_JobPosition" p
LEFT JOIN public."Hr_T_JobPosition" parent ON parent."Id" = p."ParentId"
WHERE p."IsDeleted" = FALSE AND p."DepartmentId" = v_DepartmentId
AND (p."TenantId" = v_TenantId OR (p."TenantId" IS NULL AND v_TenantId IS NULL))
AND (p."ParentId" IS NULL OR parent."DepartmentId" <> p."DepartmentId"));
IF v_JobPositionId = ANY (v_Positions) THEN
v_Positions := ARRAY(
SELECT "ParentId" FROM public."Hr_T_JobPosition"
WHERE "Id" = v_JobPositionId AND "ParentId" IS NOT NULL);
END IF;
WHILE v_Level < 10 AND cardinality(v_Positions) > 0 LOOP
v_Managers := ARRAY(
SELECT u."Id"
FROM public."AbpUsers" u
WHERE u."IsDeleted" = FALSE AND u."IsActive" = TRUE AND u."Id" <> v_EmployeeId
AND (u."TenantId" = v_TenantId OR (u."TenantId" IS NULL AND v_TenantId IS NULL))
AND u."JobPositionId" = ANY (v_Positions));
EXIT WHEN cardinality(v_Managers) > 0;
v_Positions := ARRAY(
SELECT DISTINCT p."ParentId" FROM public."Hr_T_JobPosition" p
WHERE p."Id" = ANY (v_Positions) AND p."ParentId" IS NOT NULL);
v_Level := v_Level + 1;
END LOOP;
-- Kimlikler ve adlar aynı sırada birleşir; Guard ve persona süzgeci ManagerIds içinde kimlik arar.
SELECT string_agg(u."Id"::text, '|' ORDER BY u."Id"), string_agg(u."UserName", '|' ORDER BY u."Id")
INTO v_ManagerIds, v_ManagerUserNames
FROM public."AbpUsers" u
WHERE u."Id" = ANY (v_Managers);
-- Pozisyonu en üstte olan (üst pozisyonu olmayan) çalışanın onaylayacak müdürü yoktur; müdür adımı
-- atlanır ve talep İK onayına gider. Akış bu kolona bakan karşılaştırma düğümüyle dallanır.
v_RequiresManager :=
CASE WHEN v_ManagerIds IS NULL AND EXISTS (SELECT 1 FROM public."Hr_T_JobPosition"
WHERE "Id" = v_JobPositionId AND "ParentId" IS NULL AND "IsDeleted" = FALSE)
THEN 0 ELSE 1 END;
IF v_ManagerIds IS NULL AND v_RequiresManager = 1 AND (v_Status IS NULL OR v_Status IN (v_StatusStart, v_StatusManager)) THEN
RAISE EXCEPTION 'Çalışanın departman müdürü bulunamadı. Kullanıcı kartında departman ve pozisyon tanımlı olmalı, departmanın müdür pozisyonunda aktif bir kullanıcı bulunmalıdır.'
USING ERRCODE = 'P0001', DETAIL = '51110';
END IF;
---------------------------------------------------------------------------------------------
-- Yöneticinin yapabileceği geçişler: müdür onayından İK onayına ya da redde. Başka bir
-- geçiş (ör. doğrudan Onaylandı) önceki duruma geri alınır.
---------------------------------------------------------------------------------------------
IF p_Actor = 'Manager' AND COALESCE(v_Status, '') <> COALESCE(v_LastStatus, '')
AND NOT (v_LastStatus = v_StatusManager AND v_Status IN (v_StatusHr, v_StatusRejected)) THEN
v_Status := v_LastStatus;
UPDATE public."Hr_T_LeaveRequest" SET "ApprovalStatus" = v_LastStatus WHERE "Id" = p_RequestId;
END IF;
-- Çalışanın açtığı talep doğrudan müdür onayına düşer (persona ekranında akış başlatılmaz);
-- müdürü olmayan en üst pozisyonda İK onayına.
IF v_Status IS NULL AND p_Actor = 'Employee' THEN
v_Status := CASE WHEN v_RequiresManager = 1 THEN v_StatusManager ELSE v_StatusHr END;
END IF;
-- Adıma girerken onaycı kayıttan çözülür: müdür adımında departman müdürü, İK adımında İK
-- ekranının akışındaki İK düğümü (tek kaynak; diğer ekranların düğümü yalnız geçişi yapar).
-- Adım içindeki kayıtta (karar anı) motorun yazdığı karar vereni ezmemek için dokunulmaz.
SELECT "Approver" INTO v_HrApprover
FROM public."Sas_H_ListFormWorkflow"
WHERE "ListFormCode" = 'App.Hr.LeaveRequest' AND "Kind" = 'Approval' AND "Title" = v_StatusHr
LIMIT 1;
UPDATE public."Hr_T_LeaveRequest" AS lr
SET "EmployeeName" = v_EmployeeName,
"DepartmentId" = v_DepartmentId,
"JobPositionId" = v_JobPositionId,
"ManagerIds" = COALESCE(v_ManagerIds, lr."ManagerIds"),
"ManagerUserNames" = COALESCE(v_ManagerUserNames, lr."ManagerUserNames"),
"RequiresManagerApproval" = v_RequiresManager,
"ApprovalStatus" = v_Status,
"ApprovalUser" = CASE
WHEN v_Status = COALESCE(v_LastStatus, '') THEN lr."ApprovalUser"
WHEN v_Status = v_StatusManager THEN COALESCE(v_ManagerUserNames, lr."ApprovalUser")
WHEN v_Status = v_StatusHr AND v_HrApprover IS NOT NULL THEN v_HrApprover
ELSE lr."ApprovalUser"
END,
"HalfDayPart" = CASE WHEN v_IsHalfDay = TRUE THEN COALESCE(v_HalfDayPart, 'Morning') ELSE NULL END,
"RequestNo" = CASE
WHEN lr."RequestNo" IS NULL OR rtrim(lr."RequestNo") = '' THEN
'IZN-' || EXTRACT(YEAR FROM lr."CreationTime")::int::text || '-' || right('00000' || (
SELECT COALESCE(MAX(CASE WHEN right(x."RequestNo", 5) ~ '^[0-9]+$' THEN right(x."RequestNo", 5)::int END), 0) + 1
FROM public."Hr_T_LeaveRequest" x
WHERE (x."TenantId" = v_TenantId OR (x."TenantId" IS NULL AND v_TenantId IS NULL))
AND x."RequestNo" LIKE 'IZN-' || EXTRACT(YEAR FROM x."CreationTime")::int::text || '-%')::text, 5)
ELSE lr."RequestNo"
END
WHERE lr."Id" = p_RequestId;
---------------------------------------------------------------------------------------------
-- Gün sayısı ve kurallar (sonuçlanmış talepte yeniden doğrulanmaz)
---------------------------------------------------------------------------------------------
SELECT "Code", "Gender", "MaxDaysPerYear", "DeductsFromAnnualLeave", "RequiresDocument",
"AllowHalfDay", "BalanceExceedAction"
INTO v_TypeCode, v_TypeGender, v_MaxDaysPerYear, v_Deducts, v_RequiresDocument,
v_AllowHalfDay, v_ExceedAction
FROM public."Hr_T_LeaveType"
WHERE "Id" = v_LeaveTypeId;
-- Rapor türü kendi çalışma günlerini sayar; diğer türlerde rapora denk gelen gün izinden düşmez.
v_WorkDays := public."Hr_F_LeaveWorkDays"(v_TenantId, v_StartDate::date, v_EndDate::date);
v_Days := CASE WHEN v_TypeCode = 'Sick' THEN v_WorkDays
ELSE public."Hr_F_LeaveNetDays"(v_TenantId, v_EmployeeId, v_StartDate::date, v_EndDate::date) END;
IF v_IsHalfDay = TRUE THEN
v_Days := CASE WHEN v_Days > 0.5 THEN 0.5 ELSE v_Days END;
END IF;
UPDATE public."Hr_T_LeaveRequest" SET "Days" = v_Days WHERE "Id" = p_RequestId;
IF v_Status IS NULL OR v_Status IN (v_StatusStart, v_StatusManager, v_StatusHr) THEN
IF v_TypeGender IS NOT NULL AND lower(v_TypeGender) <> 'all' AND lower(COALESCE(v_Gender, '')) <> lower(v_TypeGender) THEN
RAISE EXCEPTION 'Bu izin türü çalışanın cinsiyetine uygun değil.' USING ERRCODE = 'P0001', DETAIL = '51111';
END IF;
IF v_IsHalfDay = TRUE AND v_AllowHalfDay = FALSE THEN
RAISE EXCEPTION 'Bu izin türü yarım gün kullanılamaz.' USING ERRCODE = 'P0001', DETAIL = '51112';
END IF;
IF v_IsHalfDay = TRUE AND v_StartDate::date <> v_EndDate::date THEN
RAISE EXCEPTION 'Yarım gün izinde başlangıç ve bitiş aynı gün olmalıdır.' USING ERRCODE = 'P0001', DETAIL = '51113';
END IF;
IF v_RequiresDocument = TRUE AND (v_Document IS NULL OR rtrim(v_Document) = '') THEN
RAISE EXCEPTION 'Bu izin türü için belge (rapor vb.) eklenmelidir.' USING ERRCODE = 'P0001', DETAIL = '51114';
END IF;
IF v_WorkDays <= 0 THEN
RAISE EXCEPTION 'Seçilen tarih aralığında çalışma günü yok.' USING ERRCODE = 'P0001', DETAIL = '51115';
END IF;
IF (v_HireDate IS NOT NULL AND v_StartDate::date < v_HireDate)
OR (v_TerminationDate IS NOT NULL AND v_EndDate::date > v_TerminationDate) THEN
RAISE EXCEPTION 'İzin tarihleri çalışanın işe giriş ve işten çıkış tarihleri arasında olmalıdır.' USING ERRCODE = 'P0001', DETAIL = '51119';
END IF;
-- Çakışma: reddedilmemiş başka bir talep ya da talebe bağlı olmayan bir istirahat aynı
-- güne düşüyorsa. Aynı günün iki farklı yarısı birlikte alınabilir.
IF EXISTS (
SELECT 1 FROM public."Hr_T_LeaveRequest" o
WHERE o."EmployeeId" = v_EmployeeId AND o."Id" <> p_RequestId AND o."IsDeleted" = FALSE
AND (o."ApprovalStatus" IS NULL OR o."ApprovalStatus" <> v_StatusRejected)
AND o."StartDate"::date <= v_EndDate::date
AND o."EndDate"::date >= v_StartDate::date
AND NOT (o."IsHalfDay" = TRUE AND v_IsHalfDay = TRUE
AND COALESCE(o."HalfDayPart", 'Morning') <> COALESCE(v_HalfDayPart, 'Morning')))
OR EXISTS (
SELECT 1 FROM public."Adm_T_UserSickLeave" s
WHERE s."UserId" = v_EmployeeId AND s."IsDeleted" = FALSE
AND (v_SickLeaveId IS NULL OR s."Id" <> v_SickLeaveId)
AND NOT EXISTS (SELECT 1 FROM public."Hr_T_LeaveRequest" l WHERE l."SickLeaveId" = s."Id" AND l."IsDeleted" = FALSE)
AND s."StartDate"::date <= v_EndDate::date
AND s."EndDate"::date >= v_StartDate::date) THEN
RAISE EXCEPTION 'Bu tarihlerle çakışan başka bir izin ya da rapor kaydı var.' USING ERRCODE = 'P0001', DETAIL = '51116';
END IF;
v_Warning := NULL;
IF v_Deducts = TRUE THEN
v_Remaining := COALESCE((SELECT "RemainingDays" FROM public."Hr_V_LeaveSummary" WHERE "Id" = v_EmployeeId), 0);
v_OtherPending := COALESCE((
SELECT SUM(o."Days") FROM public."Hr_T_LeaveRequest" o
INNER JOIN public."Hr_T_LeaveType" ot ON ot."Id" = o."LeaveTypeId"
WHERE o."EmployeeId" = v_EmployeeId AND o."Id" <> p_RequestId AND o."IsDeleted" = FALSE
AND ot."DeductsFromAnnualLeave" = TRUE
AND (o."ApprovalStatus" IS NULL OR o."ApprovalStatus" IN (v_StatusStart, v_StatusManager, v_StatusHr))), 0);
IF v_Days > v_Remaining - v_OtherPending THEN
v_Warning := CONCAT('Yıllık izin bakiyesi yetersiz: kullanılabilir ',
rtrim(rtrim(to_char(round(v_Remaining - v_OtherPending, 1), 'FM999999999990.0'), '0'), '.'),
' gün, talep ', rtrim(rtrim(to_char(round(v_Days, 1), 'FM999999999990.0'), '0'), '.'), ' gün.');
END IF;
END IF;
IF v_Warning IS NULL AND v_MaxDaysPerYear IS NOT NULL THEN
v_UsedThisYear := COALESCE((
SELECT SUM(o."Days") FROM public."Hr_T_LeaveRequest" o
WHERE o."EmployeeId" = v_EmployeeId AND o."Id" <> p_RequestId AND o."IsDeleted" = FALSE
AND o."LeaveTypeId" = v_LeaveTypeId
AND EXTRACT(YEAR FROM o."StartDate") = EXTRACT(YEAR FROM v_StartDate)
AND (o."ApprovalStatus" IS NULL OR o."ApprovalStatus" <> v_StatusRejected)), 0);
IF v_UsedThisYear + v_Days > v_MaxDaysPerYear THEN
v_Warning := CONCAT('Bu izin türünün yıllık sınırı ', v_MaxDaysPerYear, ' gün; bu yıl kullanılan/bekleyen ',
rtrim(rtrim(to_char(round(v_UsedThisYear, 1), 'FM999999999990.0'), '0'), '.'),
' gün, talep ', rtrim(rtrim(to_char(round(v_Days, 1), 'FM999999999990.0'), '0'), '.'), ' gün.');
END IF;
END IF;
IF v_Warning IS NOT NULL AND COALESCE(v_ExceedAction, 'Block') = 'Block' THEN
RAISE EXCEPTION '%', v_Warning USING ERRCODE = 'P0001', DETAIL = '51117';
END IF;
UPDATE public."Hr_T_LeaveRequest" SET "BalanceWarning" = v_Warning WHERE "Id" = p_RequestId;
END IF;
---------------------------------------------------------------------------------------------
-- Rapor türü = Sağlık Kartı › İstirahatler satırı (tek kayıt). İstirahat onayla oluşur: onay
-- bekleyen rapor talebi sağlık kartında ve puantajda görünmez, izin günlerini düşürmez.
---------------------------------------------------------------------------------------------
IF v_TypeCode = 'Sick' AND v_Status = v_StatusApproved THEN
IF v_SickLeaveId IS NULL OR NOT EXISTS (SELECT 1 FROM public."Adm_T_UserSickLeave" WHERE "Id" = v_SickLeaveId) THEN
v_SickLeaveId := gen_random_uuid();
INSERT INTO public."Adm_T_UserSickLeave" ("Id", "TenantId", "UserId", "StartDate", "EndDate", "Days", "SickLeaveType",
"Institution", "Clinic", "SickLeaveReason", "CreationTime", "CreatorId", "IsDeleted")
VALUES (v_SickLeaveId, v_TenantId, v_EmployeeId, v_StartDate::date, v_EndDate::date,
(v_EndDate::date - v_StartDate::date) + 1, 'Disease', NULL, NULL,
left(v_Description, 512), (NOW() AT TIME ZONE 'UTC'), p_ActorId, FALSE);
UPDATE public."Hr_T_LeaveRequest" SET "SickLeaveId" = v_SickLeaveId WHERE "Id" = p_RequestId;
ELSE
UPDATE public."Adm_T_UserSickLeave"
SET "StartDate" = v_StartDate::date,
"EndDate" = v_EndDate::date,
"Days" = (v_EndDate::date - v_StartDate::date) + 1,
"IsDeleted" = FALSE, "DeletionTime" = NULL, "DeleterId" = NULL,
"LastModificationTime" = (NOW() AT TIME ZONE 'UTC'), "LastModifierId" = p_ActorId
WHERE "Id" = v_SickLeaveId;
END IF;
ELSIF v_SickLeaveId IS NOT NULL THEN
-- Reddedilen, onayı beklemeye dönen ya da türü rapordan başka bir türe çevrilen talebin istirahati kalkar.
UPDATE public."Adm_T_UserSickLeave"
SET "IsDeleted" = TRUE, "DeletionTime" = (NOW() AT TIME ZONE 'UTC'), "DeleterId" = p_ActorId
WHERE "Id" = v_SickLeaveId AND "IsDeleted" = FALSE;
IF v_TypeCode <> 'Sick' THEN
UPDATE public."Hr_T_LeaveRequest" SET "SickLeaveId" = NULL WHERE "Id" = p_RequestId;
END IF;
END IF;
---------------------------------------------------------------------------------------------
-- Bildirim: yalnızca bildirim taşıyan durumlara geçişte, bir kez
---------------------------------------------------------------------------------------------
IF v_Status IN (v_StatusManager, v_StatusHr, v_StatusApproved, v_StatusRejected)
AND COALESCE(v_LastStatus, '') <> v_Status THEN
IF v_Status = v_StatusManager THEN
v_Recipients := v_Managers;
ELSIF v_Status = v_StatusHr THEN
-- Onaycı metni akış tasarımcısında elle yazılır; SQL Server'daki gibi harf duyarsız eşlenir.
v_Recipients := ARRAY(
SELECT DISTINCT u."Id"
FROM string_to_table(COALESCE(v_HrApprover, ''), '|') AS a(value)
INNER JOIN public."AbpUsers" u ON (lower(u."UserName") = lower(btrim(a.value, ' '))
OR lower(u."Email") = lower(btrim(a.value, ' ')))
AND u."IsDeleted" = FALSE
AND (u."TenantId" = v_TenantId OR (u."TenantId" IS NULL AND v_TenantId IS NULL))
WHERE btrim(a.value, ' ') <> '');
ELSE
v_Recipients := ARRAY[v_EmployeeId];
END IF;
v_Type := CASE WHEN v_Status IN (v_StatusManager, v_StatusHr) THEN 'IzinTalebiOnayBekliyor' ELSE 'IzinTalebiSonuclandi' END;
v_Subject := CASE WHEN v_Status IN (v_StatusManager, v_StatusHr) THEN 'App.Hr.Leave.Notification.Approval.Subject' ELSE 'App.Hr.Leave.Notification.Result.Subject' END;
v_Body := CASE WHEN v_Status IN (v_StatusManager, v_StatusHr) THEN 'App.Hr.Leave.Notification.Approval.Message' ELSE 'App.Hr.Leave.Notification.Result.Message' END;
FOREACH v_Recipient IN ARRAY v_Recipients LOOP
CALL public."Hr_S_LeaveNotify"(p_RequestId, v_Recipient, v_Type, v_Subject, v_Body, p_ActorId);
END LOOP;
UPDATE public."Hr_T_LeaveRequest" SET "LastNotifiedStatus" = v_Status WHERE "Id" = p_RequestId;
END IF;
---------------------------------------------------------------------------------------------
-- Rapor, çalışanın diğer izinlerinin gün sayısını değiştirir. Puantaj: talebin günleri yeniden
-- hesaplanır (onaylı izin İzinli, rapor Raporlu gün olur); tarihi değişen talebin artık
-- karşılıksız kalan eski günlerini hesap kendisi aralığa katar. İleri tarih yazılmaz.
---------------------------------------------------------------------------------------------
IF v_TypeCode = 'Sick' OR v_SickLeaveId IS NOT NULL THEN
CALL public."Hr_S_LeaveDaysRefresh"(v_TenantId, v_EmployeeId);
END IF;
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, v_EmployeeId, v_StartDate::date, v_EndDate::date, p_ActorId);
END
$$;

View file

@ -0,0 +1,72 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_LeaveRequestGuard"(
p_RequestId uuid,
p_ActorId uuid,
p_Actor varchar(16),
p_Operation varchar(16)
)
LANGUAGE plpgsql
AS $$
DECLARE
v_EmployeeId uuid;
v_Status varchar(50);
v_ManagerIds varchar(1024);
v_TenantId uuid;
v_StartDate date;
v_EndDate date;
BEGIN
-- Persona ekranları satırı DefaultFilter ile süzer; kayıt uçları satırı yalnızca Id ile
-- güncellediği için sahiplik ve aşama kuralı yazma tarafında burada uygulanır. Kayıttan önce
-- çalıştığı için talebin eski tarihleri de burada kapalı puantaj dönemine karşı denetlenir.
IF p_RequestId IS NULL THEN
RETURN;
END IF;
SELECT "EmployeeId", "ApprovalStatus", "ManagerIds", "TenantId", "StartDate"::date, "EndDate"::date
INTO v_EmployeeId, v_Status, v_ManagerIds, v_TenantId, v_StartDate, v_EndDate
FROM public."Hr_T_LeaveRequest"
WHERE "Id" = p_RequestId AND "IsDeleted" = FALSE;
IF v_EmployeeId IS NULL THEN
RETURN;
END IF;
IF public."Hr_F_TimesheetPeriodClosed"(v_TenantId, v_StartDate, v_EndDate) THEN
RAISE EXCEPTION 'İzin tarihleri kapalı bir puantaj dönemine düşüyor. Önce İnsan Kaynakları dönemi açmalıdır.'
USING ERRCODE = 'P0001', DETAIL = '51118';
END IF;
IF p_Actor = 'Hr' THEN
RETURN;
END IF;
IF p_Actor = 'Employee' THEN
IF v_EmployeeId <> p_ActorId THEN
RAISE EXCEPTION 'Bu izin talebi size ait değil.' USING ERRCODE = 'P0001', DETAIL = '51100';
END IF;
-- Karar verilmiş talep çalışan tarafından değiştirilemez; iptal için İK'ya başvurulur.
IF v_Status IS NOT NULL AND v_Status NOT IN ('Onaya Gönderildi', 'Müdür Onayı') THEN
RAISE EXCEPTION 'Onay sürecinde ilerlemiş ya da sonuçlanmış izin talebi değiştirilemez veya silinemez. İptal için İnsan Kaynakları ile görüşün.'
USING ERRCODE = 'P0001', DETAIL = '51101';
END IF;
RETURN;
END IF;
IF p_Actor = 'Manager' THEN
IF p_Operation = 'Delete' THEN
RAISE EXCEPTION 'Yönetici izin talebini silemez.' USING ERRCODE = 'P0001', DETAIL = '51102';
END IF;
-- ManagerIds SQL Server döneminde büyük harfli GUID metni taşıyabilir; karşılaştırma harf duyarsız.
IF p_ActorId IS NULL
OR strpos('|' || lower(COALESCE(v_ManagerIds, '')) || '|', '|' || lower(p_ActorId::text) || '|') = 0 THEN
RAISE EXCEPTION 'Bu izin talebinin onaycısı değilsiniz.' USING ERRCODE = 'P0001', DETAIL = '51103';
END IF;
IF v_Status IS NULL OR v_Status <> 'Müdür Onayı' THEN
RAISE EXCEPTION 'İzin talebi müdür onayında beklemiyor.' USING ERRCODE = 'P0001', DETAIL = '51104';
END IF;
END IF;
END
$$;

View file

@ -0,0 +1,106 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_LeaveSickLeaveSync"(
p_SickLeaveId uuid,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_TenantId uuid;
v_UserId uuid;
v_StartDate date;
v_EndDate date;
v_Reason varchar(1024);
v_IsDeleted boolean;
v_RequestId uuid;
v_OldStart date;
v_OldEnd date;
v_Days numeric(9, 1);
v_LeaveTypeId uuid;
v_ActorName varchar(256);
v_Year varchar(4);
BEGIN
-- Sağlık Kartı › İstirahatler'den (ya da kullanıcı detayındaki istirahat listesinden) girilen
-- rapor, İzin Yönetimi'nde onaylanmış "Sick" türünde talep olarak görünür (tek kayıt). Talep
-- tarafı istirahati kendisi yazar; bu prosedür yalnızca istirahat ekranından gelen değişikliği taşır.
IF p_SickLeaveId IS NULL THEN
RETURN;
END IF;
SELECT "TenantId", "UserId", "StartDate"::date, "EndDate"::date, "SickLeaveReason", "IsDeleted"
INTO v_TenantId, v_UserId, v_StartDate, v_EndDate, v_Reason, v_IsDeleted
FROM public."Adm_T_UserSickLeave"
WHERE "Id" = p_SickLeaveId;
IF v_UserId IS NULL THEN
RETURN;
END IF;
-- Eşlenen talep istirahatin önceki tarihlerini taşır; kapalı aya düşen eski ya da yeni gün varsa
-- kayıt geri alınır (puantaj ve bordro kapanmış aydır).
SELECT "Id", "StartDate"::date, "EndDate"::date
INTO v_RequestId, v_OldStart, v_OldEnd
FROM public."Hr_T_LeaveRequest"
WHERE "SickLeaveId" = p_SickLeaveId
ORDER BY "IsDeleted", "CreationTime"
LIMIT 1;
IF public."Hr_F_TimesheetPeriodClosed"(v_TenantId, v_StartDate, v_EndDate)
OR public."Hr_F_TimesheetPeriodClosed"(v_TenantId, v_OldStart, v_OldEnd) THEN
RAISE EXCEPTION 'Rapor kapalı bir puantaj dönemine düşüyor. Önce İnsan Kaynakları dönemi açmalıdır.'
USING ERRCODE = 'P0001', DETAIL = '51120';
END IF;
IF v_IsDeleted = TRUE THEN
UPDATE public."Hr_T_LeaveRequest"
SET "IsDeleted" = TRUE, "DeletionTime" = (NOW() AT TIME ZONE 'UTC'), "DeleterId" = p_ActorId
WHERE "SickLeaveId" = p_SickLeaveId AND "IsDeleted" = FALSE;
ELSE
v_Days := public."Hr_F_LeaveWorkDays"(v_TenantId, v_StartDate, v_EndDate);
IF v_RequestId IS NOT NULL THEN
UPDATE public."Hr_T_LeaveRequest"
SET "StartDate" = v_StartDate, "EndDate" = v_EndDate, "Days" = v_Days,
"IsDeleted" = FALSE, "DeletionTime" = NULL, "DeleterId" = NULL,
"LastModificationTime" = (NOW() AT TIME ZONE 'UTC'), "LastModifierId" = p_ActorId
WHERE "Id" = v_RequestId;
ELSE
-- Kiracıda rapor türü tanımlı değilse eşlenecek talep yoktur.
SELECT "Id" INTO v_LeaveTypeId
FROM public."Hr_T_LeaveType"
WHERE "Code" = 'Sick' AND "IsDeleted" = FALSE
AND ("TenantId" = v_TenantId OR ("TenantId" IS NULL AND v_TenantId IS NULL))
LIMIT 1;
IF v_LeaveTypeId IS NOT NULL THEN
v_ActorName := (SELECT "UserName" FROM public."AbpUsers" WHERE "Id" = p_ActorId);
v_Year := EXTRACT(YEAR FROM (NOW() AT TIME ZONE 'UTC'))::int::text;
-- İstirahat sağlık birimince girilmiş bir belgedir; onay akışına girmeden onaylı yazılır.
INSERT INTO public."Hr_T_LeaveRequest"
("Id", "TenantId", "RequestNo", "EmployeeId", "EmployeeName", "DepartmentId", "JobPositionId", "LeaveTypeId",
"StartDate", "EndDate", "IsHalfDay", "Days", "Description", "SickLeaveId",
"ApprovalStatus", "ApprovalUser", "ApprovalDate", "LastNotifiedStatus", "IsActive", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), v_TenantId,
'IZN-' || v_Year || '-' || right('00000' || (
SELECT COALESCE(MAX(CASE WHEN right(x."RequestNo", 5) ~ '^[0-9]+$' THEN right(x."RequestNo", 5)::int END), 0) + 1
FROM public."Hr_T_LeaveRequest" x
WHERE (x."TenantId" = v_TenantId OR (x."TenantId" IS NULL AND v_TenantId IS NULL))
AND x."RequestNo" LIKE 'IZN-' || v_Year || '-%')::text, 5),
u."Id", CONCAT(u."Name", ' ', u."Surname"), u."DepartmentId", u."JobPositionId", v_LeaveTypeId,
v_StartDate, v_EndDate, FALSE, v_Days, left(v_Reason, 1024), p_SickLeaveId,
'Onaylandı', v_ActorName, (NOW() AT TIME ZONE 'UTC'), 'Onaylandı', TRUE, (NOW() AT TIME ZONE 'UTC'), p_ActorId, FALSE
FROM public."AbpUsers" u
WHERE u."Id" = v_UserId;
END IF;
END IF;
END IF;
-- Rapora denk gelen izin günleri bakiyeye döner; puantajda raporlu günler (ve tarihi değiştiyse
-- eski günler) yeniden hesaplanır.
CALL public."Hr_S_LeaveDaysRefresh"(v_TenantId, v_UserId);
IF v_OldStart IS NOT NULL AND v_OldStart < v_StartDate THEN v_StartDate := v_OldStart; END IF;
IF v_OldEnd IS NOT NULL AND v_OldEnd > v_EndDate THEN v_EndDate := v_OldEnd; END IF;
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, v_UserId, v_StartDate, v_EndDate, p_ActorId);
END
$$;

View file

@ -0,0 +1,31 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformanceFeedbackOpen"(
p_FeedbackId uuid,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_ReviewId uuid;
v_ReviewerId uuid;
BEGIN
SELECT "ReviewId", "ReviewerId" INTO v_ReviewId, v_ReviewerId
FROM "Hr_T_PerformanceFeedback"
WHERE "Id" = p_FeedbackId AND "IsDeleted" = FALSE;
IF v_ReviewId IS NULL THEN
RETURN;
END IF;
-- Değerlendirici, değerlendirmenin yetkinlikleri üzerinden puan verir; satırlar burada açılır.
INSERT INTO "Hr_T_PerformanceFeedbackScore" ("Id", "TenantId", "FeedbackId", "SkillId", "IsActive", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), c."TenantId", p_FeedbackId, c."SkillId", TRUE, NOW() AT TIME ZONE 'UTC', p_ActorId, FALSE
FROM "Hr_T_PerformanceCompetency" c
WHERE c."ReviewId" = v_ReviewId AND c."IsDeleted" = FALSE
AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceFeedbackScore" s
WHERE s."FeedbackId" = p_FeedbackId AND s."SkillId" = c."SkillId" AND s."IsDeleted" = FALSE);
CALL public."Hr_S_PerformanceNotify"(v_ReviewId, v_ReviewerId, 'Performans360Talebi',
'App.Hr.Performance.Notification.Feedback.Subject', 'App.Hr.Performance.Notification.Feedback.Message', p_ActorId);
END
$$;

View file

@ -0,0 +1,66 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformanceGuard"(
p_ReviewId uuid,
p_UserId uuid,
p_Actor varchar(16),
p_Scope varchar(16),
p_FeedbackId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_Status varchar(16);
v_EmployeeId uuid;
v_ManagerId uuid;
v_PeriodStatus varchar(16);
v_Allowed boolean;
BEGIN
-- Kişisel ekranlar (Değerlendirmelerim, Ekibim, 360 Taleplerim) satırı listede süzer; bu kontrol
-- aynı kuralı yazma tarafında uygular. Kayıt uçları satırı yalnızca Id ile güncellediği için
-- başka birinin kaydına doğrudan yapılan istek burada reddedilir.
IF p_Actor = 'Hr' THEN
RETURN;
END IF;
SELECT rv."Status", rv."EmployeeId", rv."ManagerId", p."Status"
INTO v_Status, v_EmployeeId, v_ManagerId, v_PeriodStatus
FROM "Hr_T_PerformanceReview" rv
INNER JOIN "Hr_T_PerformancePeriod" p ON p."Id" = rv."PeriodId"
WHERE rv."Id" = p_ReviewId AND rv."IsDeleted" = FALSE;
IF v_Status IS NULL THEN
RAISE EXCEPTION 'Değerlendirme kaydı bulunamadı.' USING ERRCODE = 'P0001', DETAIL = '51000';
END IF;
IF v_PeriodStatus = 'Closed' THEN
RAISE EXCEPTION 'Dönem kapatıldığı için değerlendirme değiştirilemez.' USING ERRCODE = 'P0001', DETAIL = '51001';
END IF;
IF p_Actor = 'Employee' AND (p_UserId IS NULL OR p_UserId <> v_EmployeeId) THEN
RAISE EXCEPTION 'Yalnızca kendi değerlendirmenizi değiştirebilirsiniz.' USING ERRCODE = 'P0001', DETAIL = '51002';
END IF;
IF p_Actor = 'Manager' AND (p_UserId IS NULL OR v_ManagerId IS NULL OR p_UserId <> v_ManagerId) THEN
RAISE EXCEPTION 'Yalnızca yöneticisi olduğunuz değerlendirmeleri değiştirebilirsiniz.' USING ERRCODE = 'P0001', DETAIL = '51003';
END IF;
IF p_Actor = 'Reviewer' AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceFeedback" f
WHERE f."Id" = p_FeedbackId AND f."ReviewId" = p_ReviewId AND f."ReviewerId" = p_UserId AND f."IsDeleted" = FALSE) THEN
RAISE EXCEPTION 'Yalnızca size atanan 360 değerlendirmesini doldurabilirsiniz.' USING ERRCODE = 'P0001', DETAIL = '51004';
END IF;
-- Durum matrisi: hangi rol hangi aşamada neyi değiştirebilir.
v_Allowed := CASE
WHEN p_Scope = 'DevelopmentPlan' THEN TRUE
WHEN p_Actor = 'Employee' AND p_Scope = 'Review' THEN v_Status IN ('GoalSetting', 'SelfReview')
WHEN p_Actor = 'Employee' AND p_Scope IN ('Goal', 'Competency') THEN v_Status IN ('GoalSetting', 'SelfReview')
WHEN p_Actor = 'Manager' AND p_Scope IN ('Review', 'Goal', 'Competency') THEN v_Status IN ('GoalSetting', 'SelfReview', 'ManagerReview')
WHEN p_Actor = 'Reviewer' THEN v_Status IN ('GoalSetting', 'SelfReview', 'ManagerReview')
ELSE FALSE
END;
IF NOT v_Allowed THEN
RAISE EXCEPTION 'Değerlendirme bu aşamada bu işlem için açık değil.' USING ERRCODE = 'P0001', DETAIL = '51005';
END IF;
END
$$;

View file

@ -0,0 +1,42 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformanceNotify"(
p_ReviewId uuid,
p_RecipientId uuid,
p_TypeName varchar(64),
p_SubjectKey varchar(128),
p_MessageKey varchar(128),
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Alıcı kayıttan gelir (çalışan, yönetici ya da 360 değerlendiricisi); tipin Custom kuralları
-- yalnızca kanalı belirler. Metin TR dil anahtarından okunur, yer tutucular kayıttan dolar.
IF p_ReviewId IS NULL OR p_RecipientId IS NULL THEN
RETURN;
END IF;
INSERT INTO "Adm_T_Notification" ("Id", "TenantId", "NotificationRuleId", "NotificationChannel", "NotificationTypeId", "Identifier", "UserId", "Message", "Subject", "IsSent", "IsRead", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), rv."TenantId", r."Id", r."Channel", t."Id",
CASE WHEN r."Channel" = 'Mail' THEN u."Email" ELSE u."Id"::text END,
u."Id",
LEFT(REPLACE(REPLACE(REPLACE(COALESCE(msg."Value", p_MessageKey),
'[Employee]', COALESCE(e."Name" || ' ' || e."Surname", '')),
'[Period]', COALESCE(p."Name", '')),
'[Status]', COALESCE(st."Value", rv."Status")), 2048),
LEFT(COALESCE(sub."Value", p_SubjectKey), 256),
r."Channel" = 'UiActivity', FALSE, NOW() AT TIME ZONE 'UTC', p_ActorId, FALSE
FROM "Hr_T_PerformanceReview" rv
INNER JOIN "Hr_T_PerformancePeriod" p ON p."Id" = rv."PeriodId"
INNER JOIN "AbpUsers" e ON e."Id" = rv."EmployeeId"
INNER JOIN "AbpUsers" u ON u."Id" = p_RecipientId AND u."IsActive" = TRUE
INNER JOIN "Adm_T_NotificationType" t ON t."Name" = p_TypeName AND t."IsDeleted" = FALSE
AND (t."TenantId" = rv."TenantId" OR (t."TenantId" IS NULL AND rv."TenantId" IS NULL))
INNER JOIN "Adm_T_NotificationRule" r ON r."NotificationTypeId" = t."Id" AND r."IsActive" = TRUE AND r."IsCustomized" = FALSE
AND r."IsDeleted" = FALSE AND r."RecipientType" = 'Custom' AND r."RecipientId" IS NULL
LEFT JOIN "Sas_H_LanguageText" msg ON msg."ResourceName" = 'Platform' AND msg."Key" = p_MessageKey AND msg."CultureName" = 'tr'
LEFT JOIN "Sas_H_LanguageText" sub ON sub."ResourceName" = 'Platform' AND sub."Key" = p_SubjectKey AND sub."CultureName" = 'tr'
LEFT JOIN "Sas_H_LanguageText" st ON st."ResourceName" = 'Platform' AND st."Key" = 'App.Hr.PerformanceReview.Status.' || rv."Status" AND st."CultureName" = 'tr'
WHERE rv."Id" = p_ReviewId
AND (r."Channel" <> 'Mail' OR u."Email" IS NOT NULL);
END
$$;

View file

@ -0,0 +1,54 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformancePeriodActivate"(
p_PeriodId uuid,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_TenantId uuid;
v_ReviewIds uuid[];
v_EmployeeIds uuid[];
v_i integer;
BEGIN
SELECT "TenantId" INTO v_TenantId
FROM "Hr_T_PerformancePeriod"
WHERE "Id" = p_PeriodId AND "IsDeleted" = FALSE AND "Status" = 'Active';
IF NOT FOUND THEN
RETURN;
END IF;
-- Dönem aktif olduğunda kiracının aktif her kullanıcısına bir değerlendirme açılır; var olana
-- dokunulmaz, böylece dönem yeniden kaydedildiğinde yalnızca yeni katılan çalışanlar eklenir.
WITH created AS (
INSERT INTO "Hr_T_PerformanceReview" ("Id", "TenantId", "PeriodId", "EmployeeId", "EmployeeName", "DepartmentId", "JobPositionId", "Status", "LastStatus", "IsActive", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), v_TenantId, p_PeriodId, u."Id", LEFT(LTRIM(COALESCE(u."Name", '') || ' ' || COALESCE(u."Surname", '')), 128), u."DepartmentId", u."JobPositionId", 'GoalSetting', 'GoalSetting', TRUE, NOW() AT TIME ZONE 'UTC', p_ActorId, FALSE
FROM "AbpUsers" u
WHERE u."IsDeleted" = FALSE AND u."IsActive" = TRUE
AND (u."TenantId" = v_TenantId OR (u."TenantId" IS NULL AND v_TenantId IS NULL))
AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceReview" rv
WHERE rv."PeriodId" = p_PeriodId AND rv."EmployeeId" = u."Id" AND rv."IsDeleted" = FALSE)
RETURNING "Id", "EmployeeId"
)
SELECT array_agg("Id"), array_agg("EmployeeId") INTO v_ReviewIds, v_EmployeeIds
FROM created;
-- Dönemin yetkinlik seti eksik olan her değerlendirmeye kopyalanır (sonradan eklenen yetkinlik dahil).
INSERT INTO "Hr_T_PerformanceCompetency" ("Id", "TenantId", "ReviewId", "SkillId", "ExpectedLevelId", "IsActive", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), rv."TenantId", rv."Id", ps."SkillId", ps."ExpectedLevelId", TRUE, NOW() AT TIME ZONE 'UTC', p_ActorId, FALSE
FROM "Hr_T_PerformanceReview" rv
INNER JOIN "Hr_T_PerformancePeriodSkill" ps ON ps."PeriodId" = rv."PeriodId" AND ps."IsDeleted" = FALSE AND ps."IsActive" = TRUE
WHERE rv."PeriodId" = p_PeriodId AND rv."IsDeleted" = FALSE AND rv."Status" <> 'Completed'
AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceCompetency" c
WHERE c."ReviewId" = rv."Id" AND c."SkillId" = ps."SkillId" AND c."IsDeleted" = FALSE);
IF v_ReviewIds IS NOT NULL THEN
FOR v_i IN 1 .. array_length(v_ReviewIds, 1) LOOP
CALL public."Hr_S_PerformanceNotify"(v_ReviewIds[v_i], v_EmployeeIds[v_i], 'PerformansDurumDegisti',
'App.Hr.Performance.Notification.Status.Subject', 'App.Hr.Performance.Notification.Status.Message', p_ActorId);
END LOOP;
END IF;
END
$$;

View file

@ -0,0 +1,117 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformanceReviewAfterSave"(
p_ReviewId uuid,
p_UserId uuid,
p_Actor varchar(16)
)
LANGUAGE plpgsql
AS $$
DECLARE
v_Status varchar(16);
v_Last varchar(16);
v_EmployeeId uuid;
v_ManagerId uuid;
v_TenantId uuid;
v_Valid boolean;
BEGIN
SELECT "Status", COALESCE("LastStatus", 'GoalSetting'), "EmployeeId", "ManagerId", "TenantId"
INTO v_Status, v_Last, v_EmployeeId, v_ManagerId, v_TenantId
FROM "Hr_T_PerformanceReview"
WHERE "Id" = p_ReviewId AND "IsDeleted" = FALSE;
IF v_Status IS NULL THEN
RETURN;
END IF;
-- Elle açılan değerlendirmede organizasyon bilgisi çalışandan, yetkinlikler dönemin setinden gelir.
-- Ad, pano kartının başlığı olduğu için kayıtta metin olarak tutulur.
UPDATE "Hr_T_PerformanceReview" rv
SET "EmployeeName" = LEFT(LTRIM(COALESCE(u."Name", '') || ' ' || COALESCE(u."Surname", '')), 128),
"DepartmentId" = COALESCE(rv."DepartmentId", u."DepartmentId"),
"JobPositionId" = COALESCE(rv."JobPositionId", u."JobPositionId")
FROM "AbpUsers" u
WHERE u."Id" = rv."EmployeeId" AND rv."Id" = p_ReviewId;
INSERT INTO "Hr_T_PerformanceCompetency" ("Id", "TenantId", "ReviewId", "SkillId", "ExpectedLevelId", "IsActive", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), rv."TenantId", rv."Id", ps."SkillId", ps."ExpectedLevelId", TRUE, NOW() AT TIME ZONE 'UTC', p_UserId, FALSE
FROM "Hr_T_PerformanceReview" rv
INNER JOIN "Hr_T_PerformancePeriodSkill" ps ON ps."PeriodId" = rv."PeriodId" AND ps."IsDeleted" = FALSE AND ps."IsActive" = TRUE
WHERE rv."Id" = p_ReviewId AND rv."Status" <> 'Completed'
AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceCompetency" c
WHERE c."ReviewId" = rv."Id" AND c."SkillId" = ps."SkillId" AND c."IsDeleted" = FALSE);
IF v_Status <> v_Last THEN
-- İK her geçişi yapar; çalışan öz değerlendirmeyi gönderir; yönetici hedefleri onaylar,
-- değerlendirmesini kalibrasyona gönderir ya da kaydı bir önceki aşamaya iade eder.
v_Valid := CASE
WHEN p_Actor = 'Hr' THEN TRUE
WHEN p_Actor = 'Employee' AND v_Last = 'SelfReview' AND v_Status = 'ManagerReview' THEN TRUE
WHEN p_Actor = 'Manager' AND v_Last = 'GoalSetting' AND v_Status = 'SelfReview' THEN TRUE
WHEN p_Actor = 'Manager' AND v_Last = 'SelfReview' AND v_Status = 'GoalSetting' THEN TRUE
WHEN p_Actor = 'Manager' AND v_Last = 'ManagerReview' AND v_Status IN ('SelfReview', 'Calibration') THEN TRUE
ELSE FALSE
END;
-- Hedef belirleme ancak ağırlıkları toplamı 100 olan en az bir hedefle kapanır.
IF v_Valid AND v_Last = 'GoalSetting' AND v_Status <> 'GoalSetting' AND NOT EXISTS (
SELECT 1 FROM "Hr_T_PerformanceGoal" g
WHERE g."ReviewId" = p_ReviewId AND g."IsDeleted" = FALSE
HAVING SUM(g."Weight") = 100) THEN
v_Valid := FALSE;
END IF;
IF NOT v_Valid THEN
UPDATE "Hr_T_PerformanceReview" SET "Status" = v_Last WHERE "Id" = p_ReviewId;
v_Status := v_Last;
ELSE
UPDATE "Hr_T_PerformanceReview"
SET "LastStatus" = v_Status,
"SelfSubmittedDate" = CASE WHEN v_Status = 'ManagerReview' THEN NOW() AT TIME ZONE 'UTC' ELSE "SelfSubmittedDate" END,
"ManagerSubmittedDate" = CASE WHEN v_Status = 'Calibration' THEN NOW() AT TIME ZONE 'UTC' ELSE "ManagerSubmittedDate" END,
"CompletedDate" = CASE WHEN v_Status = 'Completed' THEN NOW() AT TIME ZONE 'UTC' ELSE NULL END
WHERE "Id" = p_ReviewId;
IF v_Status IN ('GoalSetting', 'SelfReview', 'Completed') THEN
CALL public."Hr_S_PerformanceNotify"(p_ReviewId, v_EmployeeId, 'PerformansDurumDegisti',
'App.Hr.Performance.Notification.Status.Subject', 'App.Hr.Performance.Notification.Status.Message', p_UserId);
END IF;
IF v_Status IN ('ManagerReview', 'Completed') THEN
CALL public."Hr_S_PerformanceNotify"(p_ReviewId, v_ManagerId, 'PerformansDurumDegisti',
'App.Hr.Performance.Notification.Status.Subject', 'App.Hr.Performance.Notification.Status.Message', p_UserId);
END IF;
-- Tamamlanan değerlendirmenin yönetici seviyeleri çalışanın yetkinlik profiline yazılır
-- (Progress 0–100 → UserSkill.Level 1–5).
IF v_Status = 'Completed' THEN
UPDATE "Adm_T_UserSkill" us
SET "Level" = x.lvl, "Note" = LEFT('Performans: ' || p."Name", 512),
"LastModificationTime" = NOW() AT TIME ZONE 'UTC', "LastModifierId" = p_UserId
FROM "Hr_T_PerformanceCompetency" c
INNER JOIN "Hr_T_SkillLevel" l ON l."Id" = c."ManagerLevelId"
INNER JOIN "Hr_T_PerformanceReview" rv ON rv."Id" = c."ReviewId"
INNER JOIN "Hr_T_PerformancePeriod" p ON p."Id" = rv."PeriodId"
CROSS JOIN LATERAL (SELECT CASE WHEN l."Progress" <= 20 THEN 1 WHEN l."Progress" >= 100 THEN 5 ELSE CAST(CEILING(l."Progress" / 20.0) AS integer) END AS lvl) x
WHERE c."SkillId" = us."SkillId" AND c."ReviewId" = p_ReviewId AND c."IsDeleted" = FALSE
AND us."UserId" = v_EmployeeId AND us."IsDeleted" = FALSE
AND (us."TenantId" = v_TenantId OR (us."TenantId" IS NULL AND v_TenantId IS NULL));
INSERT INTO "Adm_T_UserSkill" ("Id", "TenantId", "UserId", "SkillId", "Level", "Note", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), v_TenantId, v_EmployeeId, c."SkillId", x.lvl, LEFT('Performans: ' || p."Name", 512), NOW() AT TIME ZONE 'UTC', p_UserId, FALSE
FROM "Hr_T_PerformanceCompetency" c
INNER JOIN "Hr_T_SkillLevel" l ON l."Id" = c."ManagerLevelId"
INNER JOIN "Hr_T_PerformanceReview" rv ON rv."Id" = c."ReviewId"
INNER JOIN "Hr_T_PerformancePeriod" p ON p."Id" = rv."PeriodId"
CROSS JOIN LATERAL (SELECT CASE WHEN l."Progress" <= 20 THEN 1 WHEN l."Progress" >= 100 THEN 5 ELSE CAST(CEILING(l."Progress" / 20.0) AS integer) END AS lvl) x
WHERE c."ReviewId" = p_ReviewId AND c."IsDeleted" = FALSE
AND NOT EXISTS (
SELECT 1 FROM "Adm_T_UserSkill" us
WHERE us."UserId" = v_EmployeeId AND us."SkillId" = c."SkillId" AND us."IsDeleted" = FALSE
AND (us."TenantId" = v_TenantId OR (us."TenantId" IS NULL AND v_TenantId IS NULL)));
END IF;
END IF;
END IF;
CALL public."Hr_S_PerformanceReviewRecalc"(p_ReviewId);
END
$$;

View file

@ -0,0 +1,86 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_PerformanceReviewRecalc"(
p_ReviewId uuid
)
LANGUAGE plpgsql
AS $$
BEGIN
IF p_ReviewId IS NULL THEN
RETURN;
END IF;
-- Gerçekleşme %: Artan hedefte Gerçekleşen/Hedef, Azalan hedefte Hedef/Gerçekleşen; 0–150'ye kırpılır.
-- Azalan hedefte sıfır gerçekleşme hedefin tamamen tutturulduğu anlamına gelir, tavan verilir.
UPDATE "Hr_T_PerformanceGoal" g
SET "AchievementRate" = (
SELECT CASE
WHEN x.rate IS NULL THEN NULL
WHEN x.rate > 150 THEN 150
WHEN x.rate < 0 THEN 0
ELSE x.rate
END
FROM (SELECT CAST(CASE
WHEN g."ActualValue" IS NULL THEN NULL
WHEN g."Direction" = 'Decreasing' AND g."ActualValue" <= 0 THEN 150
WHEN g."Direction" = 'Decreasing' THEN g."TargetValue" / g."ActualValue" * 100
ELSE g."ActualValue" / g."TargetValue" * 100
END AS numeric(18, 2)) AS rate) x)
WHERE g."ReviewId" = p_ReviewId AND g."IsDeleted" = FALSE;
-- Yetkinlik puanı yöneticinin seçtiği seviyenin Progress değeridir.
UPDATE "Hr_T_PerformanceCompetency" c
SET "Score" = (
SELECT l."Progress"
FROM "Hr_T_SkillLevel" l
WHERE l."Id" = c."ManagerLevelId" AND l."IsDeleted" = FALSE)
WHERE c."ReviewId" = p_ReviewId AND c."IsDeleted" = FALSE;
UPDATE "Hr_T_PerformanceFeedback" f
SET "Score" = (
SELECT CAST(AVG(CAST(l."Progress" AS numeric(9, 2))) AS numeric(6, 2))
FROM "Hr_T_PerformanceFeedbackScore" s
INNER JOIN "Hr_T_SkillLevel" l ON l."Id" = s."LevelId" AND l."IsDeleted" = FALSE
WHERE s."FeedbackId" = f."Id" AND s."IsDeleted" = FALSE)
WHERE f."ReviewId" = p_ReviewId AND f."IsDeleted" = FALSE;
UPDATE "Hr_T_PerformanceReview" rv
SET "GoalScore" = s.goal_score,
"CompetencyScore" = s.competency_score,
"Feedback360Score" = s.feedback_score,
"OverallScore" = CASE
WHEN s.goal_score IS NULL AND s.competency_score IS NULL THEN NULL
ELSE CAST((COALESCE(s.goal_score, 0) * s.goal_weight + COALESCE(s.competency_score, 0) * s.competency_weight) / 100 AS numeric(6, 2))
END
FROM (
SELECT r."Id",
p."GoalWeight" AS goal_weight,
p."CompetencyWeight" AS competency_weight,
(SELECT CASE WHEN COUNT(*) = 0 THEN NULL
ELSE CAST(SUM(COALESCE(g."AchievementRate", 0) * g."Weight") / 100 AS numeric(6, 2)) END
FROM "Hr_T_PerformanceGoal" g
WHERE g."ReviewId" = r."Id" AND g."IsDeleted" = FALSE) AS goal_score,
(SELECT CAST(AVG(c."Score") AS numeric(6, 2))
FROM "Hr_T_PerformanceCompetency" c
WHERE c."ReviewId" = r."Id" AND c."IsDeleted" = FALSE AND c."Score" IS NOT NULL) AS competency_score,
(SELECT CAST(AVG(f."Score") AS numeric(6, 2))
FROM "Hr_T_PerformanceFeedback" f
WHERE f."ReviewId" = r."Id" AND f."IsDeleted" = FALSE AND f."Status" = 'Submitted' AND f."Score" IS NOT NULL) AS feedback_score
FROM "Hr_T_PerformanceReview" r
INNER JOIN "Hr_T_PerformancePeriod" p ON p."Id" = r."PeriodId"
WHERE r."Id" = p_ReviewId
) s
WHERE rv."Id" = s."Id";
-- Derece kalibre puandan, yoksa genel puandan; aralık alt sınırı dahil, üst sınırı hariçtir.
UPDATE "Hr_T_PerformanceReview" rv
SET "RatingId" = (
SELECT rt."Id"
FROM "Hr_T_PerformanceRating" rt
WHERE rt."IsDeleted" = FALSE AND rt."IsActive" = TRUE
AND (rt."TenantId" = rv."TenantId" OR (rt."TenantId" IS NULL AND rv."TenantId" IS NULL))
AND COALESCE(rv."CalibratedScore", rv."OverallScore") >= rt."MinScore"
AND COALESCE(rv."CalibratedScore", rv."OverallScore") < rt."MaxScore"
ORDER BY rt."Level" DESC
LIMIT 1)
WHERE rv."Id" = p_ReviewId;
END
$$;

View file

@ -0,0 +1,461 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_TimesheetCalculate"(
p_TenantId uuid,
p_EmployeeId uuid DEFAULT NULL,
p_From date DEFAULT NULL,
p_To date DEFAULT NULL,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_From date := p_From;
v_To date := p_To;
v_Today date := LOCALTIMESTAMP::date;
v_OrphanFrom date;
v_OrphanTo date;
v_Last date;
v_LimitText text;
v_Limit numeric(9, 2);
v_WeeklyLimit integer;
v_Now timestamp;
BEGIN
-- Günlük puantajı hareketlerden, izin/rapor/eğitim kayıtlarından ve Çalışma Takvimi'nden kurar.
-- p_EmployeeId boşsa kiracının bütün çalışanları. Kapalı dönemin günleri yazılmaz (yalnız haftalık
-- birikime girer); elle düzeltilmiş gün (IsManual) girdilerini korur, yalnız süreleri yeniden hesaplanır.
-- İzin/rapor kaydını yazan prosedürler de bunu çağırdığı için hata fırlatmaz.
-- Devam takibi çalışana göre iki türlüdür: PDKS Kart No'su olan ve okutması gelmeye başlamış çalışanda
-- okutmasız iş günü devamsızdır; diğer (elle takip edilen) çalışanda kayıtsız iş günü vardiya saatleriyle "geldi" sayılır
-- (IsAssumed), İK yalnız gelinmeyen günü işaretler.
-- Süre farkları T-SQL DATEDIFF(minute) gibi dakika sınırı sayısıdır (saniyeler kesilerek alınır).
IF v_From IS NULL OR v_To IS NULL OR v_To < v_From THEN
RETURN;
END IF;
-- Tek çalışanlık hesapta, kaynağı artık olmayan izin/rapor/eğitim günü (talebin tarihi değişti,
-- rapor ya da katılım silindi) aralığa katılır; böylece eski günler aynı işlemde düzelir.
IF p_EmployeeId IS NOT NULL THEN
SELECT MIN(t."WorkDate"), MAX(t."WorkDate")
INTO v_OrphanFrom, v_OrphanTo
FROM public."Hr_T_Timesheet" t
WHERE t."EmployeeId" = p_EmployeeId AND t."IsDeleted" = FALSE AND t."IsManual" = FALSE
AND t."DayType" IN ('Leave', 'Sick', 'Training')
AND (t."WorkDate" < v_From OR t."WorkDate" > v_To)
AND NOT EXISTS (SELECT 1 FROM public."Adm_T_UserSickLeave" s
WHERE s."UserId" = t."EmployeeId" AND s."IsDeleted" = FALSE
AND t."WorkDate" BETWEEN s."StartDate"::date AND s."EndDate"::date)
AND NOT EXISTS (SELECT 1 FROM public."Hr_T_LeaveRequest" q
WHERE q."EmployeeId" = t."EmployeeId" AND q."IsDeleted" = FALSE AND q."ApprovalStatus" = 'Onaylandı'
AND t."WorkDate" BETWEEN q."StartDate"::date AND q."EndDate"::date)
AND NOT EXISTS (SELECT 1 FROM public."Hr_T_TrainingParticipant" p
INNER JOIN public."Hr_T_Training" g ON g."Id" = p."TrainingId" AND g."IsDeleted" = FALSE
WHERE p."UserId" = t."EmployeeId" AND p."IsDeleted" = FALSE AND p."AttendanceStatus" = 'Attended'
AND t."WorkDate" BETWEEN g."StartDate" AND g."EndDate");
IF v_OrphanFrom < v_From THEN v_From := v_OrphanFrom; END IF;
IF v_OrphanTo > v_To THEN v_To := v_OrphanTo; END IF;
END IF;
-- Fazla mesai haftalık birikimle hesaplandığı için aralık tam haftalara (Pzt–Paz) genişler.
-- 1900-01-01 Pazartesidir; gün sırası oturum ayarlarından bağımsızdır.
v_From := v_From - ((v_From - DATE '1900-01-01') % 7);
v_To := v_To + (6 - ((v_To - DATE '1900-01-01') % 7));
v_Last := CASE WHEN v_To > v_Today THEN v_Today ELSE v_To END;
IF v_Last < v_From THEN
RETURN;
END IF;
v_LimitText := btrim(replace(public."Hr_F_HrSetting"(p_TenantId, 'App.HrSettings.Attendance.WeeklyLimitHours'), ',', '.'));
IF v_LimitText ~ '^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)$' THEN
BEGIN
v_Limit := v_LimitText::numeric(9, 2);
EXCEPTION WHEN numeric_value_out_of_range THEN
v_Limit := NULL;
END;
END IF;
v_WeeklyLimit := trunc(60 * COALESCE(v_Limit, 45))::integer;
-- Hafta tatili İzin Yönetimi'nin çalışma günü ayarlarıdır (tek kaynak); tanım yoksa Pzt–Cum.
DROP TABLE IF EXISTS tc_work;
CREATE TEMP TABLE tc_work (DayIndex integer PRIMARY KEY, IsWork boolean) ON COMMIT DROP;
INSERT INTO tc_work (DayIndex, IsWork)
SELECT d.DayIndex,
CASE
WHEN s.Value IS NULL THEN d.DayIndex < 5
WHEN lower(s.Value) = 'true' THEN TRUE
ELSE FALSE
END
FROM (VALUES
(0, 'App.HrSettings.Leave.WorkMonday'),
(1, 'App.HrSettings.Leave.WorkTuesday'),
(2, 'App.HrSettings.Leave.WorkWednesday'),
(3, 'App.HrSettings.Leave.WorkThursday'),
(4, 'App.HrSettings.Leave.WorkFriday'),
(5, 'App.HrSettings.Leave.WorkSaturday'),
(6, 'App.HrSettings.Leave.WorkSunday')) d (DayIndex, SettingName)
CROSS JOIN LATERAL (SELECT public."Hr_F_HrSetting"(p_TenantId, d.SettingName)::text AS Value) s;
---------------------------------------------------------------------------------------------
-- Çalışanlar: aralıkta istihdam edilen aktif kullanıcılar + hareketi ya da puantajı olanlar
---------------------------------------------------------------------------------------------
DROP TABLE IF EXISTS tc_emp;
CREATE TEMP TABLE tc_emp
(
EmployeeId uuid PRIMARY KEY, HireDate date NULL, TerminationDate date NULL,
ShiftId uuid NULL, StartMin integer, EndMin integer, BreakMin integer, LateTol integer, EarlyTol integer,
CardSince date NULL
) ON COMMIT DROP;
-- Kartlı takip: kullanıcı kartında PDKS Kart No varsa ilk PDKS/aktarım okutmasının gününden başlar;
-- öncesi (kart yeni verildiyse) ve elle girilen hareketler elle takibi değiştirmez.
INSERT INTO tc_emp (EmployeeId, HireDate, TerminationDate, ShiftId, StartMin, EndMin, BreakMin, LateTol, EarlyTol, CardSince)
SELECT u."Id", u."HireDate"::date, u."TerminationDate"::date,
s."ShiftId", s."StartMin", s."EndMin", s."BreakMin", s."LateTol", s."EarlyTol",
CASE WHEN NULLIF(btrim(u."PdksCardNo"), '') IS NOT NULL THEN
(SELECT MIN(l."WorkDate") FROM public."Hr_T_AttendanceLog" l
WHERE l."EmployeeId" = u."Id" AND l."IsDeleted" = FALSE AND l."ProcessError" IS NULL AND l."Source" <> 'Manual')
END
FROM public."AbpUsers" u
INNER JOIN public."Hr_V_AttendanceShift" s ON s."EmployeeId" = u."Id"
WHERE u."IsDeleted" = FALSE
AND (u."TenantId" = p_TenantId OR (u."TenantId" IS NULL AND p_TenantId IS NULL))
AND (p_EmployeeId IS NULL OR u."Id" = p_EmployeeId)
AND (
(u."IsActive" = TRUE AND u."HireDate" IS NOT NULL AND u."HireDate"::date <= v_Last
AND (u."TerminationDate" IS NULL OR u."TerminationDate"::date >= v_From))
OR EXISTS (SELECT 1 FROM public."Hr_T_AttendanceLog" l
WHERE l."EmployeeId" = u."Id" AND l."IsDeleted" = FALSE AND l."WorkDate" BETWEEN v_From AND v_Last)
OR EXISTS (SELECT 1 FROM public."Hr_T_Timesheet" x
WHERE x."EmployeeId" = u."Id" AND x."IsDeleted" = FALSE AND x."WorkDate" BETWEEN v_From AND v_Last)
);
IF NOT EXISTS (SELECT 1 FROM tc_emp) THEN
RETURN;
END IF;
---------------------------------------------------------------------------------------------
-- Günler: hafta tatili, takvim türü, kapalı dönem
---------------------------------------------------------------------------------------------
DROP TABLE IF EXISTS tc_day;
CREATE TEMP TABLE tc_day (WorkDate date PRIMARY KEY, IsWorkDay boolean, CalendarType varchar(16) NULL, IsClosed boolean) ON COMMIT DROP;
INSERT INTO tc_day (WorkDate, IsWorkDay, CalendarType, IsClosed)
SELECT d.WorkDate,
COALESCE(w.IsWork, FALSE),
public."Hr_F_WorkCalendarDayType"(p_TenantId, d.WorkDate),
EXISTS (
SELECT 1 FROM public."Hr_T_TimesheetPeriod" p
WHERE p."IsDeleted" = FALSE AND p."Status" = 'Closed'
AND (p."TenantId" = p_TenantId OR (p."TenantId" IS NULL AND p_TenantId IS NULL))
AND p."PeriodYear" = EXTRACT(YEAR FROM d.WorkDate)::integer
AND p."PeriodMonth" = EXTRACT(MONTH FROM d.WorkDate)::integer)
FROM (SELECT g::date AS WorkDate FROM generate_series(v_From, v_Last, INTERVAL '1 day') g) d
LEFT JOIN tc_work w ON w.DayIndex = (d.WorkDate - DATE '1900-01-01') % 7;
---------------------------------------------------------------------------------------------
-- Çalışan × gün satırları
---------------------------------------------------------------------------------------------
DROP TABLE IF EXISTS tc_row;
CREATE TEMP TABLE tc_row
(
EmployeeId uuid NOT NULL, WorkDate date NOT NULL,
ShiftId uuid NULL, StartMin integer, EndMin integer, ShiftBreak integer, LateTol integer, EarlyTol integer,
IsEmployed boolean NOT NULL DEFAULT TRUE, HasCard boolean NOT NULL DEFAULT FALSE, IsAssumed boolean NOT NULL DEFAULT FALSE,
IsWorkDay boolean, CalendarType varchar(16) NULL, IsClosed boolean,
ExistingId uuid NULL, IsManual boolean NOT NULL DEFAULT FALSE, HalfDayLeave boolean NOT NULL DEFAULT FALSE,
LeaveTypeId uuid NULL, TrainingId uuid NULL,
DayType varchar(16) NULL, FirstIn timestamp NULL, LastOut timestamp NULL, HasEvents boolean NOT NULL DEFAULT FALSE,
ScanBreak integer NOT NULL DEFAULT 0, BreakMinutes integer NOT NULL DEFAULT 0, NetMinutes integer NOT NULL DEFAULT 0,
LateMinutes integer NOT NULL DEFAULT 0, EarlyLeaveMinutes integer NOT NULL DEFAULT 0, OvertimeMinutes integer NOT NULL DEFAULT 0,
IsIncomplete boolean NOT NULL DEFAULT FALSE,
PRIMARY KEY (EmployeeId, WorkDate)
) ON COMMIT DROP;
INSERT INTO tc_row (EmployeeId, WorkDate, ShiftId, StartMin, EndMin, ShiftBreak, LateTol, EarlyTol,
IsEmployed, HasCard, IsWorkDay, CalendarType, IsClosed)
SELECT e.EmployeeId, d.WorkDate, e.ShiftId, e.StartMin, e.EndMin, e.BreakMin, e.LateTol, e.EarlyTol,
CASE WHEN e.HireDate IS NOT NULL AND e.HireDate <= d.WorkDate
AND (e.TerminationDate IS NULL OR e.TerminationDate >= d.WorkDate) THEN TRUE ELSE FALSE END,
CASE WHEN e.CardSince <= d.WorkDate THEN TRUE ELSE FALSE END, d.IsWorkDay, d.CalendarType, d.IsClosed
FROM tc_emp e
CROSS JOIN tc_day d;
UPDATE tc_row r
SET ExistingId = t."Id", IsManual = t."IsManual"
FROM (
SELECT DISTINCT ON (x."EmployeeId", x."WorkDate") x."EmployeeId", x."WorkDate", x."Id", x."IsManual"
FROM public."Hr_T_Timesheet" x
WHERE x."IsDeleted" = FALSE AND x."WorkDate" BETWEEN v_From AND v_Last
AND (x."TenantId" = p_TenantId OR (x."TenantId" IS NULL AND p_TenantId IS NULL))
AND x."EmployeeId" IN (SELECT EmployeeId FROM tc_emp)
ORDER BY x."EmployeeId", x."WorkDate", x."IsManual" DESC NULLS LAST, x."CreationTime" NULLS FIRST
) t
WHERE t."EmployeeId" = r.EmployeeId AND t."WorkDate" = r.WorkDate;
-- Kapalı dönemin günü olduğu gibi kalır; elle düzeltilmiş günün girdileri korunur.
UPDATE tc_row r
SET DayType = t."DayType", LeaveTypeId = t."LeaveTypeId", TrainingId = t."TrainingId", FirstIn = t."FirstIn", LastOut = t."LastOut",
BreakMinutes = t."BreakMinutes",
NetMinutes = CASE WHEN r.IsClosed = TRUE THEN t."NetMinutes" ELSE 0 END,
LateMinutes = CASE WHEN r.IsClosed = TRUE THEN t."LateMinutes" ELSE 0 END,
EarlyLeaveMinutes = CASE WHEN r.IsClosed = TRUE THEN t."EarlyLeaveMinutes" ELSE 0 END,
OvertimeMinutes = CASE WHEN r.IsClosed = TRUE THEN ROUND(t."OvertimeHours" * 60, 0)::integer ELSE 0 END,
IsIncomplete = t."IsIncomplete",
IsAssumed = CASE WHEN r.IsClosed = TRUE THEN t."IsAssumed" ELSE FALSE END
FROM public."Hr_T_Timesheet" t
WHERE t."Id" = r.ExistingId
AND (r.IsClosed = TRUE OR r.IsManual = TRUE);
-- Elle devamsız/izinli/raporlu işaretlenen günde saat tutulmaz.
UPDATE tc_row
SET FirstIn = NULL, LastOut = NULL, BreakMinutes = 0
WHERE IsManual = TRUE AND IsClosed = FALSE AND DayType IN ('Absent', 'Leave', 'Sick');
---------------------------------------------------------------------------------------------
-- Hareketler: ilk giriş, son çıkış, okutulan mola. Yönü "Auto" olan okutma sıraya göre
-- giriş/çıkış sayılır (tek sıra giriş, çift sıra çıkış).
-- Yön büyük/küçük harf duyarsız karşılaştırılır (SQL Server harmanlamasıyla aynı sonuç).
---------------------------------------------------------------------------------------------
DROP TABLE IF EXISTS tc_event;
CREATE TEMP TABLE tc_event (EmployeeId uuid, WorkDate date, EventTime timestamp, Dir varchar(8),
NextTime timestamp NULL, NextDir varchar(8) NULL) ON COMMIT DROP;
WITH ev AS (
SELECT l."EmployeeId" AS EmployeeId, l."WorkDate" AS WorkDate, l."EventTime" AS EventTime,
MIN(lower(l."Direction")) AS Direction
FROM public."Hr_T_AttendanceLog" l
WHERE l."IsDeleted" = FALSE AND l."ProcessError" IS NULL AND l."WorkDate" BETWEEN v_From AND v_Last
AND (l."TenantId" = p_TenantId OR (l."TenantId" IS NULL AND p_TenantId IS NULL))
AND l."EmployeeId" IN (SELECT EmployeeId FROM tc_emp)
GROUP BY l."EmployeeId", l."WorkDate", l."EventTime"
), numbered AS (
SELECT ev.*, ROW_NUMBER() OVER (PARTITION BY ev.EmployeeId, ev.WorkDate ORDER BY ev.EventTime) AS Rn
FROM ev
), dir AS (
SELECT n.EmployeeId, n.WorkDate, n.EventTime,
CASE WHEN n.Direction = 'in' THEN 'In'
WHEN n.Direction = 'out' THEN 'Out'
WHEN n.Rn % 2 = 1 THEN 'In' ELSE 'Out' END AS Dir
FROM numbered n
)
INSERT INTO tc_event (EmployeeId, WorkDate, EventTime, Dir, NextTime, NextDir)
SELECT dir.EmployeeId, dir.WorkDate, dir.EventTime, dir.Dir,
LEAD(dir.EventTime) OVER (PARTITION BY dir.EmployeeId, dir.WorkDate ORDER BY dir.EventTime),
LEAD(dir.Dir) OVER (PARTITION BY dir.EmployeeId, dir.WorkDate ORDER BY dir.EventTime)
FROM dir;
UPDATE tc_row r
SET HasEvents = TRUE,
FirstIn = COALESCE(s.FirstIn, s.FirstAny),
LastOut = CASE WHEN s.LastOut > COALESCE(s.FirstIn, s.FirstAny) THEN s.LastOut END
FROM (
SELECT e.EmployeeId, e.WorkDate,
MIN(CASE WHEN e.Dir = 'In' THEN e.EventTime END) AS FirstIn,
MIN(e.EventTime) AS FirstAny,
MAX(CASE WHEN e.Dir = 'Out' THEN e.EventTime END) AS LastOut
FROM tc_event e
GROUP BY e.EmployeeId, e.WorkDate
) s
WHERE s.EmployeeId = r.EmployeeId AND s.WorkDate = r.WorkDate
AND r.IsClosed = FALSE AND r.IsManual = FALSE;
-- Okutulan mola: ilk giriş ile son çıkış arasındaki çıkış → giriş aralıkları.
UPDATE tc_row r
SET ScanBreak = COALESCE((
SELECT SUM((EXTRACT(EPOCH FROM date_trunc('minute', e.NextTime) - date_trunc('minute', e.EventTime)) / 60)::integer)
FROM tc_event e
WHERE e.EmployeeId = r.EmployeeId AND e.WorkDate = r.WorkDate
AND e.Dir = 'Out' AND e.NextDir = 'In'
AND e.EventTime >= r.FirstIn AND e.NextTime <= r.LastOut), 0)
WHERE r.IsClosed = FALSE AND r.IsManual = FALSE AND r.FirstIn IS NOT NULL AND r.LastOut IS NOT NULL;
---------------------------------------------------------------------------------------------
-- Gün tipi (öncelik sırasıyla):
-- Raporlu: takvim günü; SGK istirahati hafta sonunu da kapsar.
-- Ücretsiz izin (IsPaid = 0; doğum izni dahil): takvim günü; hafta tatili ve genel tatil ücreti doğmaz.
-- Resmî tatil, Hafta tatili: ücretli izne denk gelen tatil izinden sayılmaz (4857 md. 56);
-- İzin Yönetimi'nin gün sayısıyla aynı kural.
-- Ücretli izin > okutmalı Çalışma > Eğitim (katılımcı "Katıldı") > Çalışma / Devamsız.
-- Okutması olmayan iş günü: kartlı çalışanda devamsız (bugün henüz değil), kartsız çalışanda
-- (bugün dahil) vardiya saatleriyle çalışılmış sayılır.
---------------------------------------------------------------------------------------------
UPDATE tc_row r
SET HalfDayLeave = CASE WHEN x.LvIsHalfDay = TRUE THEN TRUE ELSE FALSE END,
LeaveTypeId = x.LvLeaveTypeId,
TrainingId = x.TrTrainingId,
DayType = CASE
WHEN x.SkId IS NOT NULL THEN 'Sick'
WHEN x.LvId IS NOT NULL AND x.LvIsPaid = FALSE AND (x.LvIsHalfDay = FALSE OR r.FirstIn IS NULL) THEN 'Leave'
WHEN r.CalendarType = 'Holiday' THEN 'Holiday'
WHEN r.IsWorkDay = FALSE THEN 'WeeklyOff'
WHEN x.LvId IS NOT NULL AND (x.LvIsHalfDay = FALSE OR r.FirstIn IS NULL) THEN 'Leave'
WHEN r.FirstIn IS NOT NULL THEN 'Work'
WHEN x.TrTrainingId IS NOT NULL THEN 'Training'
WHEN r.HasCard = FALSE THEN 'Work'
WHEN r.WorkDate < v_Today THEN 'Absent'
END
FROM (
SELECT r2.EmployeeId, r2.WorkDate, sk.Id AS SkId,
lv.Id AS LvId, lv.IsHalfDay AS LvIsHalfDay, lv.LeaveTypeId AS LvLeaveTypeId, lv.IsPaid AS LvIsPaid,
tr.TrainingId AS TrTrainingId
FROM tc_row r2
LEFT JOIN LATERAL (
SELECT s."Id" AS Id
FROM public."Adm_T_UserSickLeave" s
WHERE s."UserId" = r2.EmployeeId AND s."IsDeleted" = FALSE
AND r2.WorkDate BETWEEN s."StartDate"::date AND s."EndDate"::date
LIMIT 1
) sk ON TRUE
LEFT JOIN LATERAL (
SELECT q."Id" AS Id, q."IsHalfDay" AS IsHalfDay, q."LeaveTypeId" AS LeaveTypeId, lt."IsPaid" AS IsPaid
FROM public."Hr_T_LeaveRequest" q
INNER JOIN public."Hr_T_LeaveType" lt ON lt."Id" = q."LeaveTypeId"
WHERE q."EmployeeId" = r2.EmployeeId AND q."IsDeleted" = FALSE AND q."ApprovalStatus" = 'Onaylandı'
AND r2.WorkDate BETWEEN q."StartDate"::date AND q."EndDate"::date
ORDER BY q."IsHalfDay" NULLS FIRST
LIMIT 1
) lv ON TRUE
LEFT JOIN LATERAL (
SELECT p."TrainingId" AS TrainingId
FROM public."Hr_T_TrainingParticipant" p
INNER JOIN public."Hr_T_Training" g ON g."Id" = p."TrainingId" AND g."IsDeleted" = FALSE
WHERE p."UserId" = r2.EmployeeId AND p."IsDeleted" = FALSE AND p."AttendanceStatus" = 'Attended'
AND r2.WorkDate BETWEEN g."StartDate" AND g."EndDate"
ORDER BY g."StartDate" NULLS FIRST
LIMIT 1
) tr ON TRUE
WHERE r2.IsClosed = FALSE AND r2.IsManual = FALSE
) x
WHERE x.EmployeeId = r.EmployeeId AND x.WorkDate = r.WorkDate;
-- İstihdam dışındaki hareketsiz gün yazılmaz (işe girişten önce, çıkıştan sonra).
UPDATE tc_row
SET DayType = NULL
WHERE IsClosed = FALSE AND IsManual = FALSE AND IsEmployed = FALSE AND HasEvents = FALSE;
-- Elle takip edilen çalışanın kayıtsız iş günü ve okutmasız eğitim günü: vardiya saatleri
-- (arifede yarım gün). Eğitim günü haftalık birikime çalışma gibi girer.
UPDATE tc_row
SET IsAssumed = TRUE,
FirstIn = WorkDate + StartMin * INTERVAL '1 minute',
LastOut = WorkDate + (StartMin + CASE WHEN CalendarType = 'HalfDay'
THEN ((EndMin - StartMin + 1440) % 1440) / 2
ELSE (EndMin - StartMin + 1440) % 1440 END) * INTERVAL '1 minute'
WHERE IsClosed = FALSE AND IsManual = FALSE AND DayType IN ('Work', 'Training') AND HasEvents = FALSE;
---------------------------------------------------------------------------------------------
-- Süreler: mola = MAX(vardiya molası, okutulan mola); net = son çıkış − ilk giriş − mola.
-- Geç kalma / erken çıkış toleransı aşılırsa dakikanın tamamı yazılır. Yarım gün (arife)
-- vardiya süresinin yarısında biter; yarım gün izinli günde geç/erken yazılmaz.
---------------------------------------------------------------------------------------------
UPDATE tc_row r
SET BreakMinutes = CASE
WHEN m.Gross <= 0 THEN 0
WHEN r.IsManual = TRUE THEN CASE WHEN r.BreakMinutes > m.Gross THEN m.Gross ELSE r.BreakMinutes END
WHEN (CASE WHEN r.ScanBreak > r.ShiftBreak THEN r.ScanBreak ELSE r.ShiftBreak END) > m.Gross THEN m.Gross
ELSE CASE WHEN r.ScanBreak > r.ShiftBreak THEN r.ScanBreak ELSE r.ShiftBreak END
END,
IsIncomplete = CASE WHEN r.FirstIn IS NOT NULL AND r.LastOut IS NULL THEN TRUE ELSE FALSE END,
LateMinutes = CASE
WHEN r.DayType = 'Work' AND r.HalfDayLeave = FALSE AND r.FirstIn IS NOT NULL AND m.LateDiff > r.LateTol
THEN m.LateDiff
ELSE 0 END,
EarlyLeaveMinutes = CASE
WHEN r.DayType = 'Work' AND r.HalfDayLeave = FALSE AND r.LastOut IS NOT NULL AND m.EarlyDiff > r.EarlyTol
THEN m.EarlyDiff
ELSE 0 END
FROM (
SELECT r2.EmployeeId, r2.WorkDate,
CASE WHEN r2.FirstIn IS NOT NULL AND r2.LastOut IS NOT NULL
THEN (EXTRACT(EPOCH FROM date_trunc('minute', r2.LastOut) - date_trunc('minute', r2.FirstIn)) / 60)::integer
ELSE 0 END AS Gross,
(EXTRACT(EPOCH FROM date_trunc('minute', r2.FirstIn)
- (r2.WorkDate + r2.StartMin * INTERVAL '1 minute')) / 60)::integer AS LateDiff,
(EXTRACT(EPOCH FROM (r2.WorkDate + (r2.StartMin + CASE WHEN r2.CalendarType = 'HalfDay'
THEN ((r2.EndMin - r2.StartMin + 1440) % 1440) / 2
ELSE (r2.EndMin - r2.StartMin + 1440) % 1440 END) * INTERVAL '1 minute')
- date_trunc('minute', r2.LastOut)) / 60)::integer AS EarlyDiff
FROM tc_row r2
WHERE r2.IsClosed = FALSE AND r2.DayType IS NOT NULL
) m
WHERE m.EmployeeId = r.EmployeeId AND m.WorkDate = r.WorkDate;
UPDATE tc_row
SET NetMinutes = CASE
WHEN FirstIn IS NOT NULL AND LastOut IS NOT NULL
AND (EXTRACT(EPOCH FROM date_trunc('minute', LastOut) - date_trunc('minute', FirstIn)) / 60)::integer > BreakMinutes
THEN (EXTRACT(EPOCH FROM date_trunc('minute', LastOut) - date_trunc('minute', FirstIn)) / 60)::integer - BreakMinutes
ELSE 0 END
WHERE IsClosed = FALSE AND DayType IS NOT NULL;
-- Negatif çıkmasın: tolerans içindeki erken giriş geç kalma değildir.
UPDATE tc_row SET LateMinutes = 0 WHERE LateMinutes < 0;
UPDATE tc_row SET EarlyLeaveMinutes = 0 WHERE EarlyLeaveMinutes < 0;
---------------------------------------------------------------------------------------------
-- Fazla mesai (4857 md. 41): hafta tatili ve resmî tatilde çalışılan sürenin tamamı; diğer
-- günlerde haftanın kümülatif net süresi haftalık sınırı aştığı günden itibaren aşan kısım.
---------------------------------------------------------------------------------------------
UPDATE tc_row r
SET OvertimeMinutes = CASE
WHEN r.DayType IN ('WeeklyOff', 'Holiday') THEN r.NetMinutes
ELSE (CASE WHEN w.Cumulative > v_WeeklyLimit THEN w.Cumulative - v_WeeklyLimit ELSE 0 END)
- (CASE WHEN w.Cumulative - r.NetMinutes > v_WeeklyLimit THEN w.Cumulative - r.NetMinutes - v_WeeklyLimit ELSE 0 END)
END
FROM (
SELECT r2.EmployeeId, r2.WorkDate,
SUM(CASE WHEN r2.DayType IN ('WeeklyOff', 'Holiday') THEN 0 ELSE r2.NetMinutes END)
OVER (PARTITION BY r2.EmployeeId, (r2.WorkDate - DATE '1900-01-01') / 7
ORDER BY r2.WorkDate ROWS UNBOUNDED PRECEDING) AS Cumulative
FROM tc_row r2
WHERE r2.DayType IS NOT NULL
) w
WHERE w.EmployeeId = r.EmployeeId AND w.WorkDate = r.WorkDate
AND r.IsClosed = FALSE;
---------------------------------------------------------------------------------------------
-- Yazma
---------------------------------------------------------------------------------------------
v_Now := (NOW() AT TIME ZONE 'UTC');
UPDATE public."Hr_T_Timesheet" t
SET "ShiftId" = r.ShiftId,
"DayType" = r.DayType,
"LeaveTypeId" = r.LeaveTypeId,
"TrainingId" = r.TrainingId,
"FirstIn" = r.FirstIn,
"LastOut" = r.LastOut,
"BreakMinutes" = r.BreakMinutes,
"NetMinutes" = r.NetMinutes,
"NetHours" = CAST(r.NetMinutes / 60.0 AS numeric(6, 2)),
"LateMinutes" = r.LateMinutes,
"EarlyLeaveMinutes" = r.EarlyLeaveMinutes,
"OvertimeHours" = CAST(r.OvertimeMinutes / 60.0 AS numeric(6, 2)),
"IsIncomplete" = r.IsIncomplete,
"IsAssumed" = r.IsAssumed,
"CalculatedAt" = v_Now,
"LastModificationTime" = v_Now,
"LastModifierId" = COALESCE(p_ActorId, t."LastModifierId")
FROM tc_row r
WHERE r.ExistingId = t."Id" AND r.IsClosed = FALSE AND r.DayType IS NOT NULL;
-- Artık karşılığı olmayan otomatik gün (ör. geriye dönük işten çıkış) kalkar.
UPDATE public."Hr_T_Timesheet" t
SET "IsDeleted" = TRUE, "DeletionTime" = v_Now, "DeleterId" = p_ActorId
FROM tc_row r
WHERE r.ExistingId = t."Id" AND r.IsClosed = FALSE AND r.IsManual = FALSE AND r.DayType IS NULL;
-- PeriodKey SQL Server'daki CONVERT(nvarchar(36), uniqueidentifier) ile aynı (büyük harf) yazılır.
INSERT INTO public."Hr_T_Timesheet"
("Id", "TenantId", "EmployeeId", "WorkDate", "PeriodYear", "PeriodMonth", "PeriodKey", "ShiftId", "DayType", "LeaveTypeId", "TrainingId",
"FirstIn", "LastOut", "BreakMinutes", "NetMinutes", "NetHours", "LateMinutes", "EarlyLeaveMinutes", "OvertimeHours",
"IsManual", "IsIncomplete", "IsAssumed", "CalculatedAt", "CreationTime", "CreatorId", "IsDeleted")
SELECT gen_random_uuid(), p_TenantId, r.EmployeeId, r.WorkDate,
EXTRACT(YEAR FROM r.WorkDate)::integer, EXTRACT(MONTH FROM r.WorkDate)::integer,
upper(r.EmployeeId::text) || '-' || (EXTRACT(YEAR FROM r.WorkDate)::integer * 100 + EXTRACT(MONTH FROM r.WorkDate)::integer)::text,
r.ShiftId, r.DayType, r.LeaveTypeId, r.TrainingId, r.FirstIn, r.LastOut, r.BreakMinutes, r.NetMinutes,
CAST(r.NetMinutes / 60.0 AS numeric(6, 2)), r.LateMinutes, r.EarlyLeaveMinutes,
CAST(r.OvertimeMinutes / 60.0 AS numeric(6, 2)), FALSE, r.IsIncomplete, r.IsAssumed, v_Now, v_Now, p_ActorId, FALSE
FROM tc_row r
WHERE r.IsClosed = FALSE AND r.ExistingId IS NULL AND r.DayType IS NOT NULL;
END;
$$;

View file

@ -0,0 +1,70 @@
-- Zamanlanmış işin BeforeSp'si PostgreSQL'de "SELECT * FROM public.<ad>()" ile çağrıldığı için
-- prosedür değil, void dönen fonksiyondur.
CREATE OR REPLACE FUNCTION public."Hr_S_TimesheetNightly"()
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
v_Today date := LOCALTIMESTAMP::date;
v_Yesterday date := LOCALTIMESTAMP::date - 1;
v_MonthStart date;
v_PrevStart date;
v_TenantId uuid;
v_PeriodId uuid;
v_From date;
BEGIN
-- Zamanlanmış iş (Timesheet Nightly, her gece 02:00). Hareketi olmayan günleri (devamsız, hafta
-- tatili, resmî tatil, izinli, raporlu) kurar ve geriye dönük onaylanan izin/rapor, takvim ya da
-- vardiya değişikliğini yansıtır: dünün ayı ve kapanmamışsa bir önceki ay, işlenmemiş ve kartı sonradan
-- tanımlanan hareketler.
-- İş host ve her kiracı için çağrılır, prosedür parametre almaz; bu yüzden veritabanındaki bütün
-- kiracıları işler ve aynı gün işlenmiş kiracıyı (dönemin NightlyAt değeri) atlar.
v_MonthStart := make_date(EXTRACT(YEAR FROM v_Yesterday)::integer, EXTRACT(MONTH FROM v_Yesterday)::integer, 1);
v_PrevStart := (v_MonthStart - INTERVAL '1 month')::date;
-- Kart numarası kullanıcı kartına sonradan girilen okutmalar yeniden işlenir.
UPDATE public."Hr_T_AttendanceLog" l
SET "IsProcessed" = FALSE, "ProcessError" = NULL
WHERE l."IsDeleted" = FALSE AND l."ProcessError" = 'UnknownCard'
AND EXISTS (SELECT 1 FROM public."AbpUsers" u
WHERE (lower(u."PdksCardNo") = lower(l."CardNo") OR lower(u."RegistrationNo") = lower(l."CardNo")) AND u."IsDeleted" = FALSE
AND (u."TenantId" = l."TenantId" OR (u."TenantId" IS NULL AND l."TenantId" IS NULL)));
CALL public."Hr_S_AttendanceProcess"(FALSE);
FOR v_TenantId IN
SELECT DISTINCT "TenantId" FROM public."AbpUsers" WHERE "IsDeleted" = FALSE AND "HireDate" IS NOT NULL
LOOP
v_PeriodId := (SELECT "Id" FROM public."Hr_T_TimesheetPeriod"
WHERE "IsDeleted" = FALSE
AND "PeriodYear" = EXTRACT(YEAR FROM v_Yesterday)::integer
AND "PeriodMonth" = EXTRACT(MONTH FROM v_Yesterday)::integer
AND ("TenantId" = v_TenantId OR ("TenantId" IS NULL AND v_TenantId IS NULL))
LIMIT 1);
-- Ay dönemi gece işinde kendiliğinden açılır.
IF v_PeriodId IS NULL THEN
v_PeriodId := gen_random_uuid();
INSERT INTO public."Hr_T_TimesheetPeriod" ("Id", "TenantId", "PeriodYear", "PeriodMonth", "Status", "CreationTime", "IsDeleted")
VALUES (v_PeriodId, v_TenantId, EXTRACT(YEAR FROM v_Yesterday)::integer, EXTRACT(MONTH FROM v_Yesterday)::integer,
'Open', (NOW() AT TIME ZONE 'UTC'), FALSE);
END IF;
IF NOT EXISTS (SELECT 1 FROM public."Hr_T_TimesheetPeriod" WHERE "Id" = v_PeriodId AND "NightlyAt" >= v_Today::timestamp) THEN
v_From := CASE WHEN EXISTS (
SELECT 1 FROM public."Hr_T_TimesheetPeriod"
WHERE "IsDeleted" = FALSE AND "Status" = 'Closed'
AND "PeriodYear" = EXTRACT(YEAR FROM v_PrevStart)::integer
AND "PeriodMonth" = EXTRACT(MONTH FROM v_PrevStart)::integer
AND ("TenantId" = v_TenantId OR ("TenantId" IS NULL AND v_TenantId IS NULL)))
THEN v_MonthStart ELSE v_PrevStart END;
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, NULL, v_From, v_Yesterday, NULL);
UPDATE public."Hr_T_TimesheetPeriod"
SET "NightlyAt" = LOCALTIMESTAMP, "CalculatedAt" = (NOW() AT TIME ZONE 'UTC')
WHERE "Id" = v_PeriodId;
END IF;
END LOOP;
END;
$$;

View file

@ -0,0 +1,68 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_TimesheetPeriodAfterSave"(
p_PeriodId uuid,
p_ActorId uuid
)
LANGUAGE plpgsql
AS $$
DECLARE
v_TenantId uuid;
v_Year integer;
v_Month integer;
v_Status varchar(16);
v_ClosedAt timestamp;
v_From date;
v_To date;
BEGIN
-- Puantaj Dönemleri kaydı: açık dönemin her kaydı ayı bütün çalışanlar için yeniden hesaplar;
-- kapatma son kez hesaplayıp ayı kilitler. Kapalı ayın hareketleri ve günleri değiştirilemez.
SELECT "TenantId", "PeriodYear", "PeriodMonth", "Status", "ClosedAt"
INTO v_TenantId, v_Year, v_Month, v_Status, v_ClosedAt
FROM public."Hr_T_TimesheetPeriod"
WHERE "Id" = p_PeriodId AND "IsDeleted" = FALSE;
IF v_Year IS NULL THEN
RETURN;
END IF;
IF v_Month NOT BETWEEN 1 AND 12 OR v_Year NOT BETWEEN 2000 AND 2100 THEN
RAISE EXCEPTION 'Dönem yılı 2000–2100, ayı 1–12 arasında olmalıdır.' USING ERRCODE = 'P0001', DETAIL = '51220';
END IF;
IF EXISTS (SELECT 1 FROM public."Hr_T_TimesheetPeriod"
WHERE "Id" <> p_PeriodId AND "IsDeleted" = FALSE AND "PeriodYear" = v_Year AND "PeriodMonth" = v_Month
AND ("TenantId" = v_TenantId OR ("TenantId" IS NULL AND v_TenantId IS NULL))) THEN
RAISE EXCEPTION 'Bu yıl ve ay için puantaj dönemi zaten var.' USING ERRCODE = 'P0001', DETAIL = '51221';
END IF;
v_From := make_date(v_Year, v_Month, 1);
v_To := (v_From + INTERVAL '1 month - 1 day')::date;
IF v_Status = 'Closed' AND v_ClosedAt IS NOT NULL THEN
RETURN;
END IF;
-- Kapalı aya izin yazılamadığı için ayda onay bekleyen talep varken dönem kapanmaz; aksi hâlde
-- sonradan onaylanan izin puantaja hiç yansımazdı.
IF v_Status = 'Closed' AND EXISTS (
SELECT 1 FROM public."Hr_T_LeaveRequest" q
WHERE q."IsDeleted" = FALSE
AND (q."TenantId" = v_TenantId OR (q."TenantId" IS NULL AND v_TenantId IS NULL))
AND (q."ApprovalStatus" IS NULL OR q."ApprovalStatus" IN ('Onaya Gönderildi', 'Müdür Onayı', 'İK Onayı'))
AND q."StartDate"::date <= v_To AND q."EndDate"::date >= v_From) THEN
RAISE EXCEPTION 'Bu ayda onay bekleyen izin talebi var. Talepler sonuçlanmadan dönem kapatılamaz.'
USING ERRCODE = 'P0001', DETAIL = '51223';
END IF;
-- Kapatma anında da son hesap yapılır; hesap kapalı ayı yazmadığı için önce açık sayılır.
UPDATE public."Hr_T_TimesheetPeriod" SET "Status" = 'Open' WHERE "Id" = p_PeriodId;
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, NULL, v_From, v_To, p_ActorId);
UPDATE public."Hr_T_TimesheetPeriod"
SET "Status" = COALESCE(v_Status, 'Open'),
"CalculatedAt" = (NOW() AT TIME ZONE 'UTC'),
"ClosedAt" = CASE WHEN v_Status = 'Closed' THEN (NOW() AT TIME ZONE 'UTC') END,
"ClosedById" = CASE WHEN v_Status = 'Closed' THEN p_ActorId END
WHERE "Id" = p_PeriodId;
END;
$$;

View file

@ -0,0 +1,41 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_TrainingSync"(
p_TrainingId uuid,
p_UserId uuid DEFAULT NULL,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_TenantId uuid;
v_StartDate date;
v_EndDate date;
v_Participant uuid;
v_PlanId uuid;
BEGIN
-- Eğitim ya da katılımcı kaydından sonra çalışır (p_UserId boşsa eğitimin bütün katılımcıları):
-- "Katıldı" işaretli katılımcının eğitim günleri puantajda Eğitim olur (devamsız yazılmaz), bağlı
-- gelişim aksiyonu tamamlanır. Silinen katılımcı/eğitim ve değişen tarih için eski günleri
-- puantaj hesabı kendisi düzeltir.
SELECT "TenantId", "StartDate", "EndDate" INTO v_TenantId, v_StartDate, v_EndDate
FROM "Hr_T_Training"
WHERE "Id" = p_TrainingId;
IF v_StartDate IS NULL THEN
RETURN;
END IF;
FOR v_Participant IN
SELECT DISTINCT "UserId" FROM "Hr_T_TrainingParticipant"
WHERE "TrainingId" = p_TrainingId AND (p_UserId IS NULL OR "UserId" = p_UserId)
LOOP
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, v_Participant, v_StartDate, v_EndDate, p_ActorId);
END LOOP;
FOR v_PlanId IN
SELECT "Id" FROM "Hr_T_DevelopmentPlan"
WHERE "TrainingId" = p_TrainingId AND "IsDeleted" = FALSE AND (p_UserId IS NULL OR "EmployeeId" = p_UserId)
LOOP
CALL public."Hr_S_DevelopmentPlanSync"(v_PlanId);
END LOOP;
END
$$;

View file

@ -0,0 +1,55 @@
CREATE OR REPLACE PROCEDURE public."Hr_S_WorkCalendarAfterSave"(
p_CalendarId uuid,
p_ActorId uuid DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_TenantId uuid;
v_StartDate date;
v_EndDate date;
v_Recurrence varchar(256);
v_Shift integer;
BEGIN
-- Çalışma Takvimi satırı eklenince, değişince ya da silinince tatile denk gelen izin taleplerinin
-- gün sayısı ve puantaj yeniden hesaplanır; aksi hâlde sonradan girilen bayram izinden düşmeye,
-- puantajda çalışma günü görünmeye devam ederdi. Yıllık tekrarlı satırın bu yılki ve gelecek
-- yılki karşılığı işlenir; kapalı puantaj ayı yazılmaz.
SELECT "TenantId", "StartDate"::date, "EndDate"::date, "RecurrenceRule"
INTO v_TenantId, v_StartDate, v_EndDate, v_Recurrence
FROM public."Hr_T_WorkCalendar"
WHERE "Id" = p_CalendarId;
IF v_StartDate IS NULL THEN
RETURN;
END IF;
IF v_Recurrence = 'FREQ=YEARLY' THEN
v_Shift := EXTRACT(YEAR FROM LOCALTIMESTAMP)::integer - EXTRACT(YEAR FROM v_StartDate)::integer;
IF v_Shift > 0 THEN
v_StartDate := (v_StartDate + make_interval(years => v_Shift))::date;
v_EndDate := (v_EndDate + make_interval(years => v_Shift + 1))::date;
ELSE
v_EndDate := (v_EndDate + INTERVAL '1 year')::date;
END IF;
END IF;
UPDATE public."Hr_T_LeaveRequest" r
SET "Days" = CASE WHEN r."IsHalfDay" = TRUE AND n.Days > 0.5 THEN 0.5 ELSE n.Days END
FROM (
SELECT q."Id",
CASE WHEN t."Code" = 'Sick'
THEN public."Hr_F_LeaveWorkDays"(q."TenantId", q."StartDate"::date, q."EndDate"::date)
ELSE public."Hr_F_LeaveNetDays"(q."TenantId", q."EmployeeId", q."StartDate"::date, q."EndDate"::date)
END AS Days
FROM public."Hr_T_LeaveRequest" q
INNER JOIN public."Hr_T_LeaveType" t ON t."Id" = q."LeaveTypeId"
WHERE q."IsDeleted" = FALSE
AND (q."TenantId" = v_TenantId OR (q."TenantId" IS NULL AND v_TenantId IS NULL))
AND q."StartDate"::date <= v_EndDate AND q."EndDate"::date >= v_StartDate
) n
WHERE n."Id" = r."Id";
CALL public."Hr_S_TimesheetCalculate"(v_TenantId, NULL, v_StartDate, v_EndDate, p_ActorId);
END;
$$;

View file

@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_AttendanceLog"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"EmployeeId" uuid NULL,
"CardNo" varchar(64) NULL,
"EventTime" timestamp NOT NULL,
"Direction" varchar(8) NOT NULL DEFAULT 'Auto',
"Source" varchar(16) NULL,
"DeviceCode" varchar(64) NULL,
"Note" varchar(512) NULL,
"WorkDate" date NULL,
"IsProcessed" boolean NOT NULL DEFAULT FALSE,
"ProcessError" varchar(32) NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_AttendanceLog" PRIMARY KEY ("Id")
);
CREATE INDEX IF NOT EXISTS "IX_Hr_T_AttendanceLog_Employee" ON public."Hr_T_AttendanceLog" ("EmployeeId", "WorkDate") INCLUDE ("EventTime", "Direction", "IsDeleted", "ProcessError");
CREATE INDEX IF NOT EXISTS "IX_Hr_T_AttendanceLog_Pending" ON public."Hr_T_AttendanceLog" ("IsProcessed") WHERE "IsProcessed" = FALSE;

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_BenefitType"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Code" varchar(32) NOT NULL,
"Name" varchar(128) NOT NULL,
"DefaultAmount" numeric(18, 2) NULL,
"CurrencyId" varchar(64) NULL,
"IsTaxable" boolean NOT NULL DEFAULT FALSE,
"IsSgkSubject" boolean NOT NULL DEFAULT FALSE,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_BenefitType" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,53 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Candidate"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"FirstName" varchar(128) NOT NULL,
"LastName" varchar(128) NOT NULL,
"Email" varchar(256) NOT NULL,
"PhoneNumber" varchar(32) NULL,
"Gender" varchar(16) NULL,
"BirthDate" date NULL,
"Country" varchar(64) NULL,
"City" varchar(128) NULL,
"Address" varchar(512) NULL,
"EducationLevel" varchar(32) NULL,
"School" varchar(256) NULL,
"Major" varchar(256) NULL,
"GraduationYear" integer NULL,
"ExperienceYears" numeric(4, 1) NULL,
"CurrentCompany" varchar(256) NULL,
"CurrentTitle" varchar(256) NULL,
"ExpectedSalary" numeric(18, 2) NULL,
"CurrencyId" varchar(64) NULL,
"SourceId" uuid NULL,
"LinkedInUrl" varchar(512) NULL,
"SkillIds" varchar(2048) NULL,
"Photo" text NULL,
"CvDocument" text NULL,
"Notes" text NULL,
"KvkkConsent" boolean NOT NULL,
"KvkkConsentDate" timestamp NOT NULL,
"KvkkConsentMethod" varchar(32) NOT NULL DEFAULT 'Written',
"KvkkConsentDocument" text NULL,
"UserId" uuid NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Candidate" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_Candidate_KvkkConsent') THEN
ALTER TABLE public."Hr_T_Candidate" ADD CONSTRAINT "CK_Hr_T_Candidate_KvkkConsent" CHECK ("KvkkConsent" = TRUE);
END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_Candidate_Email" ON public."Hr_T_Candidate" ("TenantId", "Email") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_CandidateSource"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"Description" varchar(512) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_CandidateSource" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Certificate"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(200) NOT NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Certificate" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,64 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DevelopmentPlan"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"EmployeeId" uuid NOT NULL,
"ReviewId" uuid NULL,
"SkillId" uuid NULL,
"Title" varchar(256) NOT NULL,
"ActionType" varchar(16) NOT NULL DEFAULT 'Training',
"TrainingId" uuid NULL,
"CertificateId" uuid NULL,
"MentorId" uuid NULL,
"StartDate" date NULL,
"DueDate" date NULL,
"Status" varchar(16) NOT NULL DEFAULT 'Planned',
"Progress" integer NOT NULL DEFAULT 0,
"Description" varchar(1024) NULL,
"EmployeeNote" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DevelopmentPlan" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_DevelopmentPlan_ActionType') THEN
ALTER TABLE public."Hr_T_DevelopmentPlan" ADD CONSTRAINT "CK_Hr_T_DevelopmentPlan_ActionType" CHECK ("ActionType" IN ('Training', 'Mentoring', 'Project', 'Reading', 'Certificate', 'Other'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_DevelopmentPlan_Status') THEN
ALTER TABLE public."Hr_T_DevelopmentPlan" ADD CONSTRAINT "CK_Hr_T_DevelopmentPlan_Status" CHECK ("Status" IN ('Planned', 'InProgress', 'Completed', 'Cancelled'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_DevelopmentPlan_Progress') THEN
ALTER TABLE public."Hr_T_DevelopmentPlan" ADD CONSTRAINT "CK_Hr_T_DevelopmentPlan_Progress" CHECK ("Progress" BETWEEN 0 AND 100);
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_DevelopmentPlan_Dates') THEN
ALTER TABLE public."Hr_T_DevelopmentPlan" ADD CONSTRAINT "CK_Hr_T_DevelopmentPlan_Dates" CHECK ("DueDate" IS NULL OR "StartDate" IS NULL OR "DueDate" >= "StartDate");
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_DevelopmentPlan_Employee" ON public."Hr_T_DevelopmentPlan" ("EmployeeId");
CREATE INDEX IF NOT EXISTS "IX_Hr_T_DevelopmentPlan_Review" ON public."Hr_T_DevelopmentPlan" ("ReviewId");

View file

@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DisciplinaryCase"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"CaseNo" varchar(32) NULL,
"UserId" uuid NOT NULL,
"DepartmentId" uuid NULL,
"JobPositionId" uuid NULL,
"IncidentDate" date NOT NULL,
"IncidentTime" timestamp NULL,
"IncidentPlace" varchar(256) NULL,
"Subject" varchar(256) NOT NULL,
"IncidentDescription" text NOT NULL,
"WitnessUserIds" text NULL,
"ReportedById" uuid NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Open',
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DisciplinaryCase" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DisciplinaryDecision"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"CaseId" uuid NOT NULL,
"DecisionDate" date NOT NULL,
"PenaltyTypeId" uuid NOT NULL,
"EffectiveDate" date NULL,
"NotificationDate" date NULL,
"DeductionAmount" numeric(18, 4) NULL,
"DecisionText" text NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Draft',
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DisciplinaryDecision" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DisciplinaryDefense"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"CaseId" uuid NOT NULL,
"RequestDate" date NOT NULL,
"DueDate" date NOT NULL,
"ResponseDate" date NULL,
"DefenseText" text NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Requested',
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DisciplinaryDefense" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DisciplinaryFile"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"CaseId" uuid NOT NULL,
"DocumentTypeId" uuid NULL,
"FileDate" date NOT NULL,
"FileName" varchar(256) NOT NULL,
"FileContent" text NOT NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DisciplinaryFile" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DisciplinaryPenaltyType"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"Severity" integer NOT NULL DEFAULT 1,
"Description" varchar(512) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DisciplinaryPenaltyType" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Doctor"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"FullName" varchar(200) NOT NULL,
"DiplomaNo" varchar(50) NULL,
"DiplomaDate" date NULL,
"OhsCertificateNo" varchar(50) NULL,
"OhsCertificateDate" date NULL,
"IdentityNo" varchar(11) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Doctor" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DocumentType"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"GroupName" varchar(50) NOT NULL,
"Name" varchar(200) NOT NULL,
"IsRequired" boolean NOT NULL DEFAULT FALSE,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DocumentType" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Driver"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"UserId" uuid NOT NULL,
"Vehicle1Id" uuid NULL,
"Vehicle2Id" uuid NULL,
"Vehicle3Id" uuid NULL,
"Vehicle4Id" uuid NULL,
"Vehicle5Id" uuid NULL,
"TotalKm" integer NOT NULL DEFAULT 0,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Driver" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DriverAccident"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"DriverId" uuid NOT NULL,
"AccidentDate" date NOT NULL,
"Description" varchar(512) NULL,
"VehicleDocumentId" uuid NULL,
"FileName" varchar(256) NULL,
"FileContent" text NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DriverAccident" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DriverDocument"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"DriverId" uuid NOT NULL,
"VehicleDocumentId" uuid NOT NULL,
"FileName" varchar(256) NOT NULL,
"FileContent" text NOT NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DriverDocument" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_DriverPenalty"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"DriverId" uuid NOT NULL,
"PenaltyDate" date NOT NULL,
"Amount" numeric(18, 4) NULL,
"Description" varchar(512) NULL,
"VehicleDocumentId" uuid NULL,
"FileName" varchar(256) NULL,
"FileContent" text NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_DriverPenalty" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_EvaluationCriterion"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"Weight" numeric(5, 2) NOT NULL DEFAULT 1,
"Description" varchar(512) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_EvaluationCriterion" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_EventLog"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"BranchId" uuid NULL,
"LogSource" varchar(32) NOT NULL,
"UserId" uuid NOT NULL,
"EventDate" date NOT NULL,
"EventTime" varchar(5) NOT NULL,
"Importance" integer NOT NULL DEFAULT 1,
"ShiftId" uuid NULL,
"Description" text NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_EventLog" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_ExpenseType"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Code" varchar(32) NOT NULL,
"Name" varchar(128) NOT NULL,
"DefaultAmount" numeric(18, 2) NULL,
"CurrencyId" varchar(64) NULL,
"IsTaxable" boolean NOT NULL DEFAULT FALSE,
"IsSgkSubject" boolean NOT NULL DEFAULT FALSE,
"RequiresReceipt" boolean NOT NULL DEFAULT FALSE,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_ExpenseType" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Hospital"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(250) NOT NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Hospital" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_HospitalDepartment"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(200) NOT NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_HospitalDepartment" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Interview"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ApplicationId" uuid NOT NULL,
"Title" varchar(256) NULL,
"InterviewType" varchar(32) NOT NULL DEFAULT 'FaceToFace',
"StartDate" timestamp NOT NULL,
"EndDate" timestamp NOT NULL,
"Location" varchar(256) NULL,
"MeetingUrl" varchar(512) NULL,
"InterviewerUserId" uuid NOT NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Planned',
"Recommendation" varchar(32) NULL,
"OverallScore" numeric(5, 2) NULL,
"Notes" text NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Interview" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_InterviewScore"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"InterviewId" uuid NOT NULL,
"CriterionId" uuid NOT NULL,
"Score" integer NOT NULL,
"Comment" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_InterviewScore" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_InterviewScore_Score') THEN
ALTER TABLE public."Hr_T_InterviewScore" ADD CONSTRAINT "CK_Hr_T_InterviewScore_Score" CHECK ("Score" BETWEEN 1 AND 5);
END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_InterviewScore_Criterion" ON public."Hr_T_InterviewScore" ("InterviewId", "CriterionId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_JobApplication"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"CandidateId" uuid NOT NULL,
"JobPostingId" uuid NOT NULL,
"CandidateName" varchar(256) NULL,
"PostingTitle" varchar(256) NULL,
"ApplicationDate" date NOT NULL,
"SourceId" uuid NULL,
"OwnerUserId" uuid NULL,
"Stage" varchar(32) NOT NULL DEFAULT 'Applied',
"RejectionReasonId" uuid NULL,
"RejectionNote" varchar(1024) NULL,
"AverageScore" numeric(5, 2) NULL,
"SortOrder" integer NOT NULL DEFAULT 0,
"Notes" text NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_JobApplication" PRIMARY KEY ("Id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_JobApplication_CandidatePosting" ON public."Hr_T_JobApplication" ("TenantId", "CandidateId", "JobPostingId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_JobOffer"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"OfferNo" varchar(32) NULL,
"ApplicationId" uuid NOT NULL,
"CandidateName" varchar(256) NULL,
"OfferDate" date NOT NULL,
"ValidUntil" date NULL,
"BranchId" uuid NOT NULL,
"DepartmentId" uuid NOT NULL,
"JobPositionId" uuid NOT NULL,
"EmploymentType" varchar(32) NOT NULL DEFAULT 'FullTime',
"WorkModel" varchar(32) NOT NULL DEFAULT 'Office',
"StartDate" date NULL,
"GrossSalary" numeric(18, 2) NOT NULL,
"CurrencyId" varchar(64) NULL,
"BenefitCodes" varchar(1024) NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Draft',
"ResponseDate" date NULL,
"Notes" text NULL,
"ApprovalUser" varchar(256) NULL,
"ApprovalStatus" varchar(128) NULL,
"ApprovalDate" timestamp NULL,
"ApprovalDescription" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_JobOffer" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_JobOffer_GrossSalary') THEN
ALTER TABLE public."Hr_T_JobOffer" ADD CONSTRAINT "CK_Hr_T_JobOffer_GrossSalary" CHECK ("GrossSalary" >= 0);
END IF;
END
$$;

View file

@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_JobPosting"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"PostingNo" varchar(32) NULL,
"StaffRequestId" uuid NULL,
"Title" varchar(256) NOT NULL,
"BranchId" uuid NOT NULL,
"DepartmentId" uuid NOT NULL,
"JobPositionId" uuid NOT NULL,
"Headcount" integer NOT NULL DEFAULT 1,
"EmploymentType" varchar(32) NOT NULL DEFAULT 'FullTime',
"WorkModel" varchar(32) NOT NULL DEFAULT 'Office',
"Location" varchar(256) NULL,
"PublishDate" date NULL,
"ClosingDate" date NULL,
"SourceIds" varchar(1024) NULL,
"Description" text NULL,
"Requirements" text NULL,
"Status" varchar(32) NOT NULL DEFAULT 'Draft',
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_JobPosting" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_LeaveBalanceAdjustment"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"EmployeeId" uuid NOT NULL,
"AdjustmentDate" date NOT NULL,
"Days" numeric(5, 1) NOT NULL,
"Reason" varchar(512) NOT NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_LeaveBalanceAdjustment" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_LeaveBalanceAdjustment_Days') THEN
ALTER TABLE public."Hr_T_LeaveBalanceAdjustment" ADD CONSTRAINT "CK_Hr_T_LeaveBalanceAdjustment_Days" CHECK ("Days" <> 0);
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_LeaveBalanceAdjustment_Employee" ON public."Hr_T_LeaveBalanceAdjustment" ("TenantId", "EmployeeId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_LeaveRequest"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"RequestNo" varchar(32) NULL,
"EmployeeId" uuid NOT NULL,
"EmployeeName" varchar(256) NULL,
"DepartmentId" uuid NULL,
"JobPositionId" uuid NULL,
"LeaveTypeId" uuid NOT NULL,
"StartDate" timestamp NOT NULL,
"EndDate" timestamp NOT NULL,
"IsHalfDay" boolean NOT NULL DEFAULT FALSE,
"HalfDayPart" varchar(16) NULL,
"Days" numeric(5, 1) NOT NULL DEFAULT 0,
"Description" varchar(1024) NULL,
"DocumentFileName" varchar(256) NULL,
"DocumentContent" text NULL,
"BalanceWarning" varchar(512) NULL,
"SickLeaveId" uuid NULL,
"ManagerIds" varchar(1024) NULL,
"ManagerUserNames" varchar(1024) NULL,
"RequiresManagerApproval" integer NOT NULL DEFAULT 1,
"ApprovalStatus" varchar(50) NULL,
"ApprovalUser" varchar(1024) NULL,
"ApprovalDate" timestamp NULL,
"ApprovalDescription" varchar(512) NULL,
"LastNotifiedStatus" varchar(50) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_LeaveRequest" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_LeaveRequest_Dates') THEN
ALTER TABLE public."Hr_T_LeaveRequest" ADD CONSTRAINT "CK_Hr_T_LeaveRequest_Dates" CHECK ("EndDate" >= "StartDate");
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_LeaveRequest_HalfDayPart') THEN
ALTER TABLE public."Hr_T_LeaveRequest" ADD CONSTRAINT "CK_Hr_T_LeaveRequest_HalfDayPart" CHECK ("HalfDayPart" IS NULL OR "HalfDayPart" IN ('Morning', 'Afternoon'));
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_LeaveRequest_Employee" ON public."Hr_T_LeaveRequest" ("TenantId", "EmployeeId", "StartDate") WHERE "IsDeleted" = FALSE;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_LeaveRequest_SickLeave" ON public."Hr_T_LeaveRequest" ("SickLeaveId") WHERE "SickLeaveId" IS NOT NULL;

View file

@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_LeaveType"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Code" varchar(32) NOT NULL,
"Name" varchar(128) NOT NULL,
"Gender" varchar(16) NOT NULL DEFAULT 'All',
"MaxDaysPerYear" integer NULL,
"IsPaid" boolean NOT NULL DEFAULT TRUE,
"DeductsFromAnnualLeave" boolean NOT NULL DEFAULT FALSE,
"RequiresDocument" boolean NOT NULL DEFAULT FALSE,
"AllowHalfDay" boolean NOT NULL DEFAULT FALSE,
"BalanceExceedAction" varchar(16) NOT NULL DEFAULT 'Block',
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_LeaveType" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_LeaveType_BalanceExceedAction') THEN
ALTER TABLE public."Hr_T_LeaveType" ADD CONSTRAINT "CK_Hr_T_LeaveType_BalanceExceedAction" CHECK ("BalanceExceedAction" IN ('Block', 'Warn'));
END IF;
END
$$;

View file

@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceCompetency"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ReviewId" uuid NOT NULL,
"SkillId" uuid NOT NULL,
"ExpectedLevelId" uuid NULL,
"SelfLevelId" uuid NULL,
"ManagerLevelId" uuid NULL,
"Score" numeric(6, 2) NULL,
"SelfComment" varchar(1024) NULL,
"ManagerComment" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceCompetency" PRIMARY KEY ("Id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformanceCompetency_Skill" ON public."Hr_T_PerformanceCompetency" ("ReviewId", "SkillId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,41 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceFeedback"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ReviewId" uuid NOT NULL,
"ReviewerId" uuid NOT NULL,
"Relation" varchar(16) NOT NULL DEFAULT 'Peer',
"Status" varchar(16) NOT NULL DEFAULT 'Pending',
"Score" numeric(6, 2) NULL,
"Comment" varchar(2048) NULL,
"SubmittedDate" timestamp NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceFeedback" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceFeedback_Relation') THEN
ALTER TABLE public."Hr_T_PerformanceFeedback" ADD CONSTRAINT "CK_Hr_T_PerformanceFeedback_Relation" CHECK ("Relation" IN ('Peer', 'Subordinate', 'Superior', 'InternalCustomer'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceFeedback_Status') THEN
ALTER TABLE public."Hr_T_PerformanceFeedback" ADD CONSTRAINT "CK_Hr_T_PerformanceFeedback_Status" CHECK ("Status" IN ('Pending', 'Submitted'));
END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformanceFeedback_Reviewer" ON public."Hr_T_PerformanceFeedback" ("ReviewId", "ReviewerId") WHERE "IsDeleted" = FALSE;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_PerformanceFeedback_ReviewerId" ON public."Hr_T_PerformanceFeedback" ("ReviewerId");

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceFeedbackScore"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"FeedbackId" uuid NOT NULL,
"SkillId" uuid NOT NULL,
"LevelId" uuid NULL,
"Comment" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceFeedbackScore" PRIMARY KEY ("Id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformanceFeedbackScore_Skill" ON public."Hr_T_PerformanceFeedbackScore" ("FeedbackId", "SkillId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,60 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceGoal"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"ReviewId" uuid NOT NULL,
"GoalType" varchar(8) NOT NULL DEFAULT 'KPI',
"Title" varchar(256) NOT NULL,
"Description" varchar(1024) NULL,
"Direction" varchar(16) NOT NULL DEFAULT 'Increasing',
"Unit" varchar(32) NULL,
"TargetValue" numeric(18, 2) NOT NULL,
"ActualValue" numeric(18, 2) NULL,
"Weight" numeric(5, 2) NOT NULL,
"AchievementRate" numeric(6, 2) NULL,
"SelfComment" varchar(1024) NULL,
"ManagerComment" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceGoal" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceGoal_GoalType') THEN
ALTER TABLE public."Hr_T_PerformanceGoal" ADD CONSTRAINT "CK_Hr_T_PerformanceGoal_GoalType" CHECK ("GoalType" IN ('KPI', 'OKR'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceGoal_Direction') THEN
ALTER TABLE public."Hr_T_PerformanceGoal" ADD CONSTRAINT "CK_Hr_T_PerformanceGoal_Direction" CHECK ("Direction" IN ('Increasing', 'Decreasing'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceGoal_Weight') THEN
ALTER TABLE public."Hr_T_PerformanceGoal" ADD CONSTRAINT "CK_Hr_T_PerformanceGoal_Weight" CHECK ("Weight" > 0 AND "Weight" <= 100);
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceGoal_TargetValue') THEN
ALTER TABLE public."Hr_T_PerformanceGoal" ADD CONSTRAINT "CK_Hr_T_PerformanceGoal_TargetValue" CHECK ("TargetValue" > 0);
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_PerformanceGoal_Review" ON public."Hr_T_PerformanceGoal" ("ReviewId");

View file

@ -0,0 +1,48 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformancePeriod"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"StartDate" date NOT NULL,
"EndDate" date NOT NULL,
"SelfReviewDueDate" date NULL,
"ManagerReviewDueDate" date NULL,
"GoalWeight" numeric(5, 2) NOT NULL DEFAULT 60,
"CompetencyWeight" numeric(5, 2) NOT NULL DEFAULT 40,
"Is360Enabled" boolean NOT NULL DEFAULT FALSE,
"Status" varchar(16) NOT NULL DEFAULT 'Planned',
"Description" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformancePeriod" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformancePeriod_Dates') THEN
ALTER TABLE public."Hr_T_PerformancePeriod" ADD CONSTRAINT "CK_Hr_T_PerformancePeriod_Dates" CHECK ("EndDate" >= "StartDate");
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformancePeriod_Weights') THEN
ALTER TABLE public."Hr_T_PerformancePeriod" ADD CONSTRAINT "CK_Hr_T_PerformancePeriod_Weights" CHECK ("GoalWeight" >= 0 AND "CompetencyWeight" >= 0 AND "GoalWeight" + "CompetencyWeight" = 100);
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformancePeriod_Status') THEN
ALTER TABLE public."Hr_T_PerformancePeriod" ADD CONSTRAINT "CK_Hr_T_PerformancePeriod_Status" CHECK ("Status" IN ('Planned', 'Active', 'Calibration', 'Closed'));
END IF;
END
$$;

View file

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformancePeriodSkill"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"PeriodId" uuid NOT NULL,
"SkillId" uuid NOT NULL,
"ExpectedLevelId" uuid NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformancePeriodSkill" PRIMARY KEY ("Id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformancePeriodSkill_Skill" ON public."Hr_T_PerformancePeriodSkill" ("PeriodId", "SkillId") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceRating"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"Level" integer NOT NULL,
"MinScore" numeric(6, 2) NOT NULL,
"MaxScore" numeric(6, 2) NOT NULL,
"Color" varchar(16) NULL,
"TargetDistribution" numeric(5, 2) NULL,
"Description" varchar(512) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceRating" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceRating_Range') THEN
ALTER TABLE public."Hr_T_PerformanceRating" ADD CONSTRAINT "CK_Hr_T_PerformanceRating_Range" CHECK ("MinScore" < "MaxScore");
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceRating_TargetDistribution') THEN
ALTER TABLE public."Hr_T_PerformanceRating" ADD CONSTRAINT "CK_Hr_T_PerformanceRating_TargetDistribution" CHECK ("TargetDistribution" IS NULL OR "TargetDistribution" BETWEEN 0 AND 100);
END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformanceRating_Level" ON public."Hr_T_PerformanceRating" ("TenantId", "Level") WHERE "IsDeleted" = FALSE;

View file

@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_PerformanceReview"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"PeriodId" uuid NOT NULL,
"EmployeeId" uuid NOT NULL,
"EmployeeName" varchar(128) NULL,
"ManagerId" uuid NULL,
"DepartmentId" uuid NULL,
"JobPositionId" uuid NULL,
"Status" varchar(16) NOT NULL DEFAULT 'GoalSetting',
"LastStatus" varchar(16) NULL,
"GoalScore" numeric(6, 2) NULL,
"CompetencyScore" numeric(6, 2) NULL,
"Feedback360Score" numeric(6, 2) NULL,
"OverallScore" numeric(6, 2) NULL,
"CalibratedScore" numeric(6, 2) NULL,
"RatingId" uuid NULL,
"SelfComment" varchar(2048) NULL,
"ManagerComment" varchar(2048) NULL,
"CalibrationNote" varchar(1024) NULL,
"SelfSubmittedDate" timestamp NULL,
"ManagerSubmittedDate" timestamp NULL,
"CompletedDate" timestamp NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_PerformanceReview" PRIMARY KEY ("Id")
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceReview_Status') THEN
ALTER TABLE public."Hr_T_PerformanceReview" ADD CONSTRAINT "CK_Hr_T_PerformanceReview_Status" CHECK ("Status" IN ('GoalSetting', 'SelfReview', 'ManagerReview', 'Calibration', 'Completed'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_Hr_T_PerformanceReview_CalibratedScore') THEN
ALTER TABLE public."Hr_T_PerformanceReview" ADD CONSTRAINT "CK_Hr_T_PerformanceReview_CalibratedScore" CHECK ("CalibratedScore" IS NULL OR "CalibratedScore" BETWEEN 0 AND 150);
END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "UX_Hr_T_PerformanceReview_Employee" ON public."Hr_T_PerformanceReview" ("PeriodId", "EmployeeId") WHERE "IsDeleted" = FALSE;
CREATE INDEX IF NOT EXISTS "IX_Hr_T_PerformanceReview_Manager" ON public."Hr_T_PerformanceReview" ("ManagerId");

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_RejectionReason"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(128) NOT NULL,
"RejectedBy" varchar(32) NOT NULL DEFAULT 'Company',
"Description" varchar(512) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_RejectionReason" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Shift"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"Name" varchar(64) NOT NULL,
"StartTime" varchar(5) NULL,
"EndTime" varchar(5) NULL,
"BreakMinutes" integer NULL,
"WorkingHours" numeric(5, 2) NULL,
"IsNightShift" boolean NOT NULL DEFAULT FALSE,
"LateToleranceMinutes" integer NULL,
"EarlyLeaveToleranceMinutes" integer NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Shift" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_StaffRequest"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"RequestNo" varchar(32) NULL,
"RequestDate" date NOT NULL,
"BranchId" uuid NOT NULL,
"DepartmentId" uuid NOT NULL,
"JobPositionId" uuid NOT NULL,
"Headcount" integer NOT NULL DEFAULT 1,
"EmploymentType" varchar(32) NOT NULL DEFAULT 'FullTime',
"Reason" varchar(32) NOT NULL DEFAULT 'NewPosition',
"ReplacedUserId" uuid NULL,
"ExpectedStartDate" date NULL,
"MinSalary" numeric(18, 2) NULL,
"MaxSalary" numeric(18, 2) NULL,
"CurrencyId" varchar(64) NULL,
"Qualifications" text NULL,
"Description" text NULL,
"RequestedById" uuid NULL,
"ApprovalUser" varchar(256) NULL,
"ApprovalStatus" varchar(128) NULL,
"ApprovalDate" timestamp NULL,
"ApprovalDescription" varchar(1024) NULL,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_StaffRequest" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Timesheet"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"EmployeeId" uuid NOT NULL,
"WorkDate" date NOT NULL,
"PeriodYear" integer NOT NULL,
"PeriodMonth" integer NOT NULL,
"PeriodKey" varchar(48) NOT NULL,
"ShiftId" uuid NULL,
"DayType" varchar(16) NOT NULL DEFAULT 'Work',
"LeaveTypeId" uuid NULL,
"TrainingId" uuid NULL,
"FirstIn" timestamp NULL,
"LastOut" timestamp NULL,
"BreakMinutes" integer NOT NULL DEFAULT 0,
"NetMinutes" integer NOT NULL DEFAULT 0,
"NetHours" numeric(6, 2) NOT NULL DEFAULT 0,
"LateMinutes" integer NOT NULL DEFAULT 0,
"EarlyLeaveMinutes" integer NOT NULL DEFAULT 0,
"OvertimeHours" numeric(6, 2) NOT NULL DEFAULT 0,
"IsManual" boolean NOT NULL DEFAULT FALSE,
"IsIncomplete" boolean NOT NULL DEFAULT FALSE,
"IsAssumed" boolean NOT NULL DEFAULT FALSE,
"Note" varchar(512) NULL,
"CalculatedAt" timestamp NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Timesheet" PRIMARY KEY ("Id")
);
CREATE INDEX IF NOT EXISTS "IX_Hr_T_Timesheet_EmployeeDate" ON public."Hr_T_Timesheet" ("EmployeeId", "WorkDate") INCLUDE ("IsDeleted", "IsManual");
CREATE INDEX IF NOT EXISTS "IX_Hr_T_Timesheet_Period" ON public."Hr_T_Timesheet" ("TenantId", "PeriodYear", "PeriodMonth") INCLUDE ("EmployeeId", "DayType", "IsDeleted");

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_TimesheetPeriod"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"PeriodYear" integer NOT NULL,
"PeriodMonth" integer NOT NULL,
"Status" varchar(16) NOT NULL DEFAULT 'Open',
"CalculatedAt" timestamp NULL,
"ClosedAt" timestamp NULL,
"ClosedById" uuid NULL,
"NightlyAt" timestamp NULL,
"Note" varchar(512) NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_TimesheetPeriod" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_Training"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"TrainingTypeId" uuid NOT NULL,
"TrainingSubjectId" uuid NOT NULL,
"EntryDate" date NOT NULL,
"BranchId" uuid NOT NULL,
"StartDate" date NOT NULL,
"EndDate" date NOT NULL,
"StartTime" timestamp NULL,
"EndTime" timestamp NULL,
"TotalHours" numeric(18, 4) NULL,
"TrainerName" varchar(200) NULL,
"TrainingPlace" text NULL,
"TrainingInstitution" text NULL,
"Description" text NULL,
"Status" varchar(16) NOT NULL DEFAULT 'Open',
"SkillIds" text NULL,
"CostPlace" numeric(18, 4) NULL DEFAULT 0,
"CostTrainer" numeric(18, 4) NULL DEFAULT 0,
"CostAccommodation" numeric(18, 4) NULL DEFAULT 0,
"CostTransport" numeric(18, 4) NULL DEFAULT 0,
"CostCatering" numeric(18, 4) NULL DEFAULT 0,
"CostOther" numeric(18, 4) NULL DEFAULT 0,
"CostTotal" numeric(18, 4) NULL DEFAULT 0,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_Training" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_TrainingFile"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"TrainingId" uuid NOT NULL,
"DocumentTypeId" uuid NULL,
"FileDate" date NOT NULL,
"FileName" varchar(256) NOT NULL,
"FileContent" text NOT NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_TrainingFile" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_TrainingParticipant"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"TrainingId" uuid NOT NULL,
"UserId" uuid NOT NULL,
"HireDate" date NULL,
"DepartmentId" uuid NULL,
"JobPositionId" uuid NULL,
"AttendanceStatus" varchar(16) NOT NULL DEFAULT 'Attended',
"EvaluationScore" numeric(18, 4) NULL,
"ExamScore" numeric(18, 4) NULL,
"LeaveDate" date NULL,
"Note" text NULL,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_TrainingParticipant" PRIMARY KEY ("Id")
);

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS public."Hr_T_TrainingSubject"
(
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
"TenantId" uuid NULL,
"TrainingTypeId" uuid NOT NULL,
"Name" varchar(256) NOT NULL,
"IsEvaluated" boolean NOT NULL DEFAULT TRUE,
"IsActive" boolean NOT NULL DEFAULT TRUE,
"CreationTime" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
"CreatorId" uuid NULL,
"LastModificationTime" timestamp NULL,
"LastModifierId" uuid NULL,
"IsDeleted" boolean NOT NULL DEFAULT FALSE,
"DeletionTime" timestamp NULL,
"DeleterId" uuid NULL,
CONSTRAINT "PK_Hr_T_TrainingSubject" PRIMARY KEY ("Id")
);

Some files were not shown because too many files have changed in this diff Show more